Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00131668aa | ||
|
|
0306ad737f | ||
|
|
20dfc34e70 | ||
|
|
b7e8b252d2 | ||
|
|
74f09c332b | ||
|
|
3ce6c23603 | ||
|
|
ad3ee14501 | ||
|
|
1931758fc7 | ||
|
|
fe551af145 | ||
|
|
9fdaaed100 | ||
|
|
c0f8a355e3 | ||
|
|
df95ec4ce8 | ||
|
|
a15cad4d03 | ||
|
|
8904f20eb8 | ||
|
|
0ee81d0739 | ||
|
|
c0bc6f8ccb | ||
|
|
1f1cd1e236 | ||
|
|
dbbd78fb26 | ||
|
|
f6e45842fd | ||
|
|
d25c043046 | ||
|
|
5695ba7c13 | ||
|
|
eeb0322ce1 | ||
|
|
480977c2b5 | ||
|
|
8b2e46a6fa | ||
|
|
842be660b0 | ||
|
|
4f69c0e502 | ||
|
|
be128181e2 | ||
|
|
0aa1628925 | ||
|
|
fd58f669d0 | ||
|
|
51927f82c0 | ||
|
|
0f8a0eaf4a | ||
|
|
b9ac22d4ed | ||
|
|
ebca5c6409 | ||
|
|
6ef35e4dba | ||
|
|
8bb5200dd8 | ||
|
|
4aeb6b7c9b | ||
|
|
ae64d7ba82 | ||
|
|
9dd2dbff19 | ||
|
|
cebbffe230 | ||
|
|
e3e464abfa | ||
|
|
268438b0ff | ||
|
|
355c8de5e7 | ||
|
|
e6056bc4a4 | ||
|
|
f2eebe95bd | ||
|
|
3084013269 | ||
|
|
68568e5a25 | ||
|
|
0eae0bb8fe | ||
|
|
b61820d354 | ||
|
|
1606272850 | ||
|
|
ea04fe1c9b | ||
|
|
4c50b6a8af | ||
|
|
39aa848e9e | ||
|
|
64c56c1b17 | ||
|
|
8e883a6b10 | ||
|
|
9684aa3ad5 | ||
|
|
5fede84bf6 | ||
|
|
a788e07be1 | ||
|
|
68ffd2248b | ||
|
|
4b12d021e4 | ||
|
|
4daf9db9db | ||
|
|
a660b1eb05 | ||
|
|
6f2f098e12 | ||
|
|
6a75ef86b0 | ||
|
|
3d2397879c | ||
|
|
de474089a7 | ||
|
|
8b31b9aa05 | ||
|
|
ad41672958 | ||
|
|
86f2984b30 | ||
|
|
38e62786e4 | ||
|
|
ac3d7c9fae | ||
|
|
98f94ea694 | ||
|
|
8ad80f7ac3 | ||
|
|
296678046c | ||
|
|
fd193e651d | ||
|
|
becad5ee67 | ||
|
|
161dcbdd4c | ||
|
|
7ad6652cb0 | ||
|
|
c467167e91 | ||
|
|
18093824d8 | ||
|
|
8102f3c33c | ||
|
|
902c441248 | ||
|
|
e641bb3d57 | ||
|
|
19d938ed60 | ||
|
|
5561949f9b | ||
|
|
5e98cca062 | ||
|
|
ac858c9bee | ||
|
|
f58f5e4e02 | ||
|
|
ad844f5e51 | ||
|
|
53c1bc45d6 | ||
|
|
0cc93aa87e | ||
|
|
b41b830086 | ||
|
|
c79d0890c0 | ||
|
|
5071e6566f | ||
|
|
ac77fe360d | ||
|
|
711aceba0e | ||
|
|
f010279303 | ||
|
|
0c67f4940a | ||
|
|
7836aeacc6 | ||
|
|
af0cc5c899 | ||
|
|
aa2808d621 | ||
|
|
621067acd8 | ||
|
|
3fcba33b1b | ||
|
|
9a66dae0ba | ||
|
|
e2ef8d2dc8 | ||
|
|
eb089bb207 | ||
|
|
4565c3311c | ||
|
|
e30b41a85a | ||
|
|
7b0321043d | ||
|
|
c4b7062d65 | ||
|
|
eb1c1b9475 | ||
|
|
67725f0cba | ||
|
|
68c4ad2bf9 | ||
|
|
dbafda9822 | ||
|
|
6d5d2a1c4d | ||
|
|
b748b8d077 | ||
|
|
f86c448359 | ||
|
|
d0af4b8ddd | ||
|
|
39103c07c9 | ||
|
|
8382ebc057 | ||
|
|
880ba8f1dd | ||
|
|
f00849b8d1 | ||
|
|
c090f78898 | ||
|
|
46a1713122 | ||
|
|
4a88b33782 | ||
|
|
a1a2a141e4 | ||
|
|
97404455d4 | ||
|
|
be31100bd7 |
No files matched your search
@@ -38,6 +38,7 @@ BASECAMP3_USER_AGENT=
|
|||||||
DAILY_API_KEY=
|
DAILY_API_KEY=
|
||||||
DAILY_SCALE_PLAN=''
|
DAILY_SCALE_PLAN=''
|
||||||
DAILY_WEBHOOK_SECRET=''
|
DAILY_WEBHOOK_SECRET=''
|
||||||
|
DAILY_MEETING_ENDED_WEBHOOK_SECRET=''
|
||||||
|
|
||||||
# - GOOGLE CALENDAR/MEET/LOGIN
|
# - GOOGLE CALENDAR/MEET/LOGIN
|
||||||
# Needed to enable Google Calendar integration and Login with Google
|
# Needed to enable Google Calendar integration and Login with Google
|
||||||
|
|||||||
+5
-1
@@ -240,7 +240,7 @@ CSP_POLICY=
|
|||||||
# Vercel Edge Config
|
# Vercel Edge Config
|
||||||
EDGE_CONFIG=
|
EDGE_CONFIG=
|
||||||
|
|
||||||
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
||||||
NEXT_PUBLIC_BOOKER_NUMBER_OF_DAYS_TO_LOAD=0 # Override the booker to only load X number of days worth of data
|
NEXT_PUBLIC_BOOKER_NUMBER_OF_DAYS_TO_LOAD=0 # Override the booker to only load X number of days worth of data
|
||||||
|
|
||||||
# Control time intervals on a user's Schedule availability
|
# Control time intervals on a user's Schedule availability
|
||||||
@@ -347,6 +347,10 @@ 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"
|
||||||
|
|
||||||
|
# Tasker features
|
||||||
|
TASKER_ENABLE_WEBHOOKS=0
|
||||||
|
TASKER_ENABLE_EMAILS=0
|
||||||
|
|
||||||
# Ratelimiting via unkey
|
# Ratelimiting via unkey
|
||||||
UNKEY_ROOT_KEY=
|
UNKEY_ROOT_KEY=
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ env:
|
|||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
||||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||||
|
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
name: Cron - changeTimeZone
|
||||||
|
|
||||||
|
on:
|
||||||
|
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||||
|
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
||||||
|
schedule:
|
||||||
|
# Runs "At every full hour." (see https://crontab.guru)
|
||||||
|
- cron: "0 * * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cron-scheduleEmailReminders:
|
||||||
|
env:
|
||||||
|
APP_URL: ${{ secrets.APP_URL }}
|
||||||
|
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: cURL request
|
||||||
|
if: ${{ env.APP_URL && env.CRON_API_KEY }}
|
||||||
|
run: |
|
||||||
|
curl ${{ secrets.APP_URL }}/api/cron/changeTimeZone \
|
||||||
|
-X POST \
|
||||||
|
-H 'content-type: application/json' \
|
||||||
|
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
|
||||||
|
-sSf
|
||||||
@@ -62,6 +62,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
|
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||||
@@ -91,6 +92,7 @@ jobs:
|
|||||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
|
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
|
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||||
@@ -73,6 +74,7 @@ jobs:
|
|||||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
|
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
|
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||||
@@ -80,6 +81,7 @@ jobs:
|
|||||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
|
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
|
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||||
@@ -82,6 +83,7 @@ jobs:
|
|||||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
|
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ on:
|
|||||||
env:
|
env:
|
||||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
|
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||||
@@ -31,6 +32,7 @@ env:
|
|||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
||||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||||
|
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ on:
|
|||||||
env:
|
env:
|
||||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
|
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||||
@@ -35,6 +36,7 @@ env:
|
|||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
||||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||||
|
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { get } from "@vercel/edge-config";
|
||||||
|
import type { NextMiddleware } from "next-api-middleware";
|
||||||
|
|
||||||
|
const safeGet = async <T = unknown>(key: string): Promise<T | undefined> => {
|
||||||
|
try {
|
||||||
|
return get<T>(key);
|
||||||
|
} catch (error) {
|
||||||
|
// Don't crash if EDGE_CONFIG env var is missing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const config = { matcher: "/:path*" };
|
||||||
|
|
||||||
|
export const checkIsInMaintenanceMode: NextMiddleware = async (req, res, next) => {
|
||||||
|
const isInMaintenanceMode = await safeGet<boolean>("isInMaintenanceMode");
|
||||||
|
if (isInMaintenanceMode) {
|
||||||
|
return res
|
||||||
|
.status(503)
|
||||||
|
.json({ message: "API is currently under maintenance. Please try again at a later time." });
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import { label } from "next-api-middleware";
|
|||||||
|
|
||||||
import { addRequestId } from "./addRequestid";
|
import { addRequestId } from "./addRequestid";
|
||||||
import { captureErrors } from "./captureErrors";
|
import { captureErrors } from "./captureErrors";
|
||||||
|
import { checkIsInMaintenanceMode } from "./checkIsInMaintenanceMode";
|
||||||
import { extendRequest } from "./extendRequest";
|
import { extendRequest } from "./extendRequest";
|
||||||
import {
|
import {
|
||||||
HTTP_POST,
|
HTTP_POST,
|
||||||
@@ -24,6 +25,7 @@ const middleware = {
|
|||||||
HTTP_POST,
|
HTTP_POST,
|
||||||
HTTP_DELETE,
|
HTTP_DELETE,
|
||||||
addRequestId,
|
addRequestId,
|
||||||
|
checkIsInMaintenanceMode,
|
||||||
verifyApiKey,
|
verifyApiKey,
|
||||||
rateLimitApiKey,
|
rateLimitApiKey,
|
||||||
extendRequest,
|
extendRequest,
|
||||||
@@ -36,6 +38,7 @@ type Middleware = keyof typeof middleware;
|
|||||||
|
|
||||||
const middlewareOrder = [
|
const middlewareOrder = [
|
||||||
// The order here, determines the order of execution
|
// The order here, determines the order of execution
|
||||||
|
"checkIsInMaintenanceMode",
|
||||||
"extendRequest",
|
"extendRequest",
|
||||||
"captureErrors",
|
"captureErrors",
|
||||||
"verifyApiKey",
|
"verifyApiKey",
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ export const schemaUserBaseBodyParams = User.pick({
|
|||||||
brandColor: true,
|
brandColor: true,
|
||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
allowDynamicBooking: true,
|
allowDynamicBooking: true,
|
||||||
away: true,
|
|
||||||
role: true,
|
role: true,
|
||||||
// @note: disallowing avatar changes via API for now. We can add it later if needed. User should upload image via UI.
|
// @note: disallowing avatar changes via API for now. We can add it later if needed. User should upload image via UI.
|
||||||
// avatar: true,
|
// avatar: true,
|
||||||
@@ -171,7 +170,6 @@ export const schemaUserReadPublic = User.pick({
|
|||||||
brandColor: true,
|
brandColor: true,
|
||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
allowDynamicBooking: true,
|
allowDynamicBooking: true,
|
||||||
away: true,
|
|
||||||
createdDate: true,
|
createdDate: true,
|
||||||
verified: true,
|
verified: true,
|
||||||
invitedTo: true,
|
invitedTo: true,
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import type { NextApiRequest } from "next";
|
|
||||||
|
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
|
|
||||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
|
||||||
|
|
||||||
async function authMiddleware(req: NextApiRequest) {
|
|
||||||
const { userId, isAdmin } = req;
|
|
||||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
|
||||||
if (isAdmin) return;
|
|
||||||
const eventType = await prisma.eventType.findFirst({
|
|
||||||
where: { id, users: { some: { id: userId } } },
|
|
||||||
});
|
|
||||||
if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
|
||||||
}
|
|
||||||
|
|
||||||
export default authMiddleware;
|
|
||||||
@@ -4,11 +4,8 @@ import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
|||||||
|
|
||||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||||
|
|
||||||
import authMiddleware from "./_auth-middleware";
|
|
||||||
|
|
||||||
export default withMiddleware()(
|
export default withMiddleware()(
|
||||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
await authMiddleware(req);
|
|
||||||
return defaultHandler({
|
return defaultHandler({
|
||||||
GET: import("./_get"),
|
GET: import("./_get"),
|
||||||
PATCH: import("./_patch"),
|
PATCH: import("./_patch"),
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default async function checkTeamEventEditPermission(
|
|||||||
|
|
||||||
if (!membership?.role || !["ADMIN", "OWNER"].includes(membership.role)) {
|
if (!membership?.role || !["ADMIN", "OWNER"].includes(membership.role)) {
|
||||||
throw new HttpError({
|
throw new HttpError({
|
||||||
statusCode: 401,
|
statusCode: 403,
|
||||||
message: "No permission to operate on event-type for this team",
|
message: "No permission to operate on event-type for this team",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { createMocks } from "node-mocks-http";
|
|||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
|
||||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||||
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
|
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import prismaMock from "../../../../../../../tests/libs/__mocks__/prismaMock";
|
||||||
|
|
||||||
|
import type { Request, Response } from "express";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { createMocks } from "node-mocks-http";
|
||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { buildEventType } from "@calcom/lib/test/builder";
|
||||||
|
import { MembershipRole } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
|
import handler from "../../../../pages/api/event-types/[id]/_get";
|
||||||
|
|
||||||
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
|
describe("GET /api/event-types/[id]", () => {
|
||||||
|
describe("Errors", () => {
|
||||||
|
test("Returns 403 if user not admin/team member/event owner", async () => {
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
body: {},
|
||||||
|
query: {
|
||||||
|
id: 123456,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
prismaMock.eventType.findUnique.mockResolvedValue(
|
||||||
|
buildEventType({
|
||||||
|
id: 123456,
|
||||||
|
userId: 444444,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
req.userId = 333333;
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Success", async () => {
|
||||||
|
test("Returns event type if user is admin", async () => {
|
||||||
|
const eventTypeId = 123456;
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
body: {},
|
||||||
|
query: {
|
||||||
|
id: eventTypeId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
prismaMock.eventType.findUnique.mockResolvedValue(
|
||||||
|
buildEventType({
|
||||||
|
id: eventTypeId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
req.isAdmin = true;
|
||||||
|
req.userId = 333333;
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Returns event type if user is in team associated with event type", async () => {
|
||||||
|
const eventTypeId = 123456;
|
||||||
|
const teamId = 9999;
|
||||||
|
const userId = 333333;
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
body: {},
|
||||||
|
query: {
|
||||||
|
id: eventTypeId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
prismaMock.eventType.findUnique.mockResolvedValue(
|
||||||
|
buildEventType({
|
||||||
|
id: eventTypeId,
|
||||||
|
teamId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
prismaMock.team.findFirst.mockResolvedValue({
|
||||||
|
id: teamId,
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
req.isAdmin = false;
|
||||||
|
req.userId = userId;
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId);
|
||||||
|
expect(prismaMock.team.findFirst).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
id: teamId,
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId: req.userId,
|
||||||
|
role: {
|
||||||
|
in: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Returns event type if user is the event type owner", async () => {
|
||||||
|
const eventTypeId = 123456;
|
||||||
|
const userId = 333333;
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
body: {},
|
||||||
|
query: {
|
||||||
|
id: eventTypeId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
prismaMock.eventType.findUnique.mockResolvedValue(
|
||||||
|
buildEventType({
|
||||||
|
id: eventTypeId,
|
||||||
|
userId,
|
||||||
|
scheduleId: 1111,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
req.isAdmin = false;
|
||||||
|
req.userId = userId;
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId);
|
||||||
|
expect(prismaMock.team.findFirst).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
|
import { getEnv } from "@/env";
|
||||||
import { Controller, Get, Version, VERSION_NEUTRAL } from "@nestjs/common";
|
import { Controller, Get, Version, VERSION_NEUTRAL } from "@nestjs/common";
|
||||||
|
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
|
@DocsTags("Health - development only")
|
||||||
|
@DocsExcludeController(getEnv("NODE_ENV") === "production")
|
||||||
export class AppController {
|
export class AppController {
|
||||||
@Get("health")
|
@Get("health")
|
||||||
@Version(VERSION_NEUTRAL)
|
@Version(VERSION_NEUTRAL)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AppLoggerMiddleware } from "@/app.logger.middleware";
|
import { AppLoggerMiddleware } from "@/app.logger.middleware";
|
||||||
|
import { RewriterMiddleware } from "@/app.rewrites.middleware";
|
||||||
import appConfig from "@/config/app";
|
import appConfig from "@/config/app";
|
||||||
import { AuthModule } from "@/modules/auth/auth.module";
|
import { AuthModule } from "@/modules/auth/auth.module";
|
||||||
import { EndpointsModule } from "@/modules/endpoints.module";
|
import { EndpointsModule } from "@/modules/endpoints.module";
|
||||||
@@ -6,6 +7,7 @@ import { JwtModule } from "@/modules/jwt/jwt.module";
|
|||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
|
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
|
||||||
import { ConfigModule } from "@nestjs/config";
|
import { ConfigModule } from "@nestjs/config";
|
||||||
|
import { RouterModule } from "@nestjs/core";
|
||||||
|
|
||||||
import { AppController } from "./app.controller";
|
import { AppController } from "./app.controller";
|
||||||
|
|
||||||
@@ -34,11 +36,14 @@ import { AppController } from "./app.controller";
|
|||||||
EndpointsModule,
|
EndpointsModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
JwtModule,
|
JwtModule,
|
||||||
|
//register prefix for all routes in EndpointsModule
|
||||||
|
RouterModule.register([{ path: "/v2", module: EndpointsModule }]),
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
})
|
})
|
||||||
export class AppModule implements NestModule {
|
export class AppModule implements NestModule {
|
||||||
configure(consumer: MiddlewareConsumer): void {
|
configure(consumer: MiddlewareConsumer): void {
|
||||||
consumer.apply(AppLoggerMiddleware).forRoutes("*");
|
consumer.apply(AppLoggerMiddleware).forRoutes("*");
|
||||||
|
consumer.apply(RewriterMiddleware).forRoutes("/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Injectable, NestMiddleware } from "@nestjs/common";
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RewriterMiddleware implements NestMiddleware {
|
||||||
|
use(req: Request, res: Response, next: () => void) {
|
||||||
|
if (req.url.startsWith("/api/v2")) {
|
||||||
|
req.url = req.url.replace("/api/v2", "/v2");
|
||||||
|
}
|
||||||
|
if (req.url.startsWith("/v2/ee")) {
|
||||||
|
req.url = req.url.replace("/v2/ee", "/v2");
|
||||||
|
}
|
||||||
|
if (req.url.includes("reccuring")) {
|
||||||
|
req.url = req.url.replace("reccuring", "recurring");
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ import * as Sentry from "@sentry/node";
|
|||||||
import * as cookieParser from "cookie-parser";
|
import * as cookieParser from "cookie-parser";
|
||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
|
|
||||||
|
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||||
|
|
||||||
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
|
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
|
||||||
|
|
||||||
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
|
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
|
||||||
@@ -26,7 +28,7 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
|||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: "*",
|
origin: "*",
|
||||||
methods: ["GET", "PATCH", "DELETE", "HEAD", "POST", "PUT", "OPTIONS"],
|
methods: ["GET", "PATCH", "DELETE", "HEAD", "POST", "PUT", "OPTIONS"],
|
||||||
allowedHeaders: ["Accept", "Authorization", "Content-Type", "Origin"],
|
allowedHeaders: [X_CAL_CLIENT_ID, X_CAL_SECRET_KEY, "Accept", "Authorization", "Content-Type", "Origin"],
|
||||||
maxAge: 86_400,
|
maxAge: 86_400,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,10 +60,6 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
|||||||
app.useGlobalFilters(new HttpExceptionFilter());
|
app.useGlobalFilters(new HttpExceptionFilter());
|
||||||
app.useGlobalFilters(new TRPCExceptionFilter());
|
app.useGlobalFilters(new TRPCExceptionFilter());
|
||||||
|
|
||||||
app.setGlobalPrefix("api", {
|
|
||||||
exclude: [{ path: "health", method: RequestMethod.GET }],
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const loadConfig = (): AppConfig => {
|
|||||||
process.env.API_PORT && getEnv("NODE_ENV", "development") === "development"
|
process.env.API_PORT && getEnv("NODE_ENV", "development") === "development"
|
||||||
? `:${Number(getEnv("API_PORT", "5555"))}`
|
? `:${Number(getEnv("API_PORT", "5555"))}`
|
||||||
: ""
|
: ""
|
||||||
}/api/v2`,
|
}/v2`,
|
||||||
},
|
},
|
||||||
db: {
|
db: {
|
||||||
readUrl: getEnv("DATABASE_READ_URL"),
|
readUrl: getEnv("DATABASE_READ_URL"),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BookingsController } from "@/ee/bookings/controllers/bookings.controller";
|
import { BookingsController } from "@/ee/bookings/controllers/bookings.controller";
|
||||||
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
@@ -7,7 +8,7 @@ import { Module } from "@nestjs/common";
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, TokensModule],
|
imports: [PrismaModule, TokensModule],
|
||||||
providers: [TokensRepository, OAuthFlowService],
|
providers: [TokensRepository, OAuthFlowService, OAuthClientRepository],
|
||||||
controllers: [BookingsController],
|
controllers: [BookingsController],
|
||||||
})
|
})
|
||||||
export class BookingsModule {}
|
export class BookingsModule {}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import { bootstrap } from "@/app";
|
import { bootstrap } from "@/app";
|
||||||
import { AppModule } from "@/app.module";
|
import { AppModule } from "@/app.module";
|
||||||
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
|
||||||
|
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
|
||||||
|
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
|
||||||
|
import { SchedulesModule } from "@/ee/schedules/schedules.module";
|
||||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
||||||
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
@@ -11,17 +15,12 @@ import { Test } from "@nestjs/testing";
|
|||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import * as request from "supertest";
|
import * as request from "supertest";
|
||||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||||
|
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
||||||
|
|
||||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
import {
|
import { handleNewBooking } from "@calcom/platform-libraries";
|
||||||
getAllUserBookings,
|
|
||||||
handleNewBooking,
|
|
||||||
getBookingInfo,
|
|
||||||
handleNewRecurringBooking,
|
|
||||||
handleInstantMeeting,
|
|
||||||
} from "@calcom/platform-libraries";
|
|
||||||
import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
|
import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
describe("Bookings Endpoints", () => {
|
describe("Bookings Endpoints", () => {
|
||||||
@@ -30,28 +29,50 @@ describe("Bookings Endpoints", () => {
|
|||||||
|
|
||||||
let userRepositoryFixture: UserRepositoryFixture;
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||||
|
let schedulesService: SchedulesService;
|
||||||
|
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||||
|
|
||||||
const userEmail = "bookings-controller-e2e@api.com";
|
const userEmail = "bookings-controller-e2e@api.com";
|
||||||
let user: User;
|
let user: User;
|
||||||
|
|
||||||
|
let eventTypeId: number;
|
||||||
|
|
||||||
let createdBooking: Awaited<ReturnType<typeof handleNewBooking>>;
|
let createdBooking: Awaited<ReturnType<typeof handleNewBooking>>;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withAccessTokenAuth(
|
const moduleRef = await withAccessTokenAuth(
|
||||||
userEmail,
|
userEmail,
|
||||||
Test.createTestingModule({
|
Test.createTestingModule({
|
||||||
imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule],
|
imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule, SchedulesModule],
|
||||||
providers: [SchedulesRepository, SchedulesService],
|
|
||||||
})
|
})
|
||||||
).compile();
|
)
|
||||||
|
.overrideGuard(PermissionsGuard)
|
||||||
|
.useValue({
|
||||||
|
canActivate: () => true,
|
||||||
|
})
|
||||||
|
.compile();
|
||||||
|
|
||||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||||
|
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||||
|
schedulesService = moduleRef.get<SchedulesService>(SchedulesService);
|
||||||
|
|
||||||
user = await userRepositoryFixture.create({
|
user = await userRepositoryFixture.create({
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userSchedule: CreateScheduleInput = {
|
||||||
|
name: "working time",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
isDefault: true,
|
||||||
|
};
|
||||||
|
await schedulesService.createUserSchedule(user.id, userSchedule);
|
||||||
|
const event = await eventTypesRepositoryFixture.create(
|
||||||
|
{ title: "peer coding", slug: "peer-coding", length: 60 },
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
eventTypeId = event.id;
|
||||||
|
|
||||||
app = moduleRef.createNestApplication();
|
app = moduleRef.createNestApplication();
|
||||||
bootstrap(app as NestExpressApplication);
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
@@ -64,13 +85,23 @@ describe("Bookings Endpoints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should create a booking", async () => {
|
it("should create a booking", async () => {
|
||||||
const bookingStart = "2023-05-25T09:30:00.000Z";
|
const bookingStart = "2040-05-21T09:30:00.000Z";
|
||||||
const bookingEnd = "2023-05-25T10:30:00.000Z";
|
const bookingEnd = "2040-05-21T10:30:00.000Z";
|
||||||
const bookingEventTypeId = 7;
|
const bookingEventTypeId = eventTypeId;
|
||||||
const bookingTimeZone = "Europe/Londom";
|
const bookingTimeZone = "Europe/London";
|
||||||
const bookingLanguage = "en";
|
const bookingLanguage = "en";
|
||||||
const bookingHashedLink = "";
|
const bookingHashedLink = "";
|
||||||
const bookingMetadata = {};
|
const bookingMetadata = {};
|
||||||
|
const bookingResponses = {
|
||||||
|
name: "tester",
|
||||||
|
email: "tester@example.com",
|
||||||
|
location: {
|
||||||
|
value: "link",
|
||||||
|
optionValue: "",
|
||||||
|
},
|
||||||
|
notes: "test",
|
||||||
|
guests: [],
|
||||||
|
};
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
start: bookingStart,
|
start: bookingStart,
|
||||||
@@ -80,10 +111,11 @@ describe("Bookings Endpoints", () => {
|
|||||||
language: bookingLanguage,
|
language: bookingLanguage,
|
||||||
metadata: bookingMetadata,
|
metadata: bookingMetadata,
|
||||||
hashedLink: bookingHashedLink,
|
hashedLink: bookingHashedLink,
|
||||||
|
responses: bookingResponses,
|
||||||
};
|
};
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post("/api/v2/ee/bookings")
|
.post("/v2/bookings")
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(201)
|
.expect(201)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
@@ -91,8 +123,8 @@ describe("Bookings Endpoints", () => {
|
|||||||
response.body;
|
response.body;
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseBody.data).toBeDefined();
|
||||||
expect(responseBody.data.user.email).toBeDefined();
|
expect(responseBody.data.userPrimaryEmail).toBeDefined();
|
||||||
expect(responseBody.data.user.email).toEqual(userEmail);
|
expect(responseBody.data.userPrimaryEmail).toEqual(userEmail);
|
||||||
expect(responseBody.data.id).toBeDefined();
|
expect(responseBody.data.id).toBeDefined();
|
||||||
expect(responseBody.data.uid).toBeDefined();
|
expect(responseBody.data.uid).toBeDefined();
|
||||||
expect(responseBody.data.startTime).toEqual(bookingStart);
|
expect(responseBody.data.startTime).toEqual(bookingStart);
|
||||||
@@ -106,10 +138,10 @@ describe("Bookings Endpoints", () => {
|
|||||||
|
|
||||||
it("should get bookings", async () => {
|
it("should get bookings", async () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.get("/api/v2/ee/bookings")
|
.get("/v2/bookings?filters[status]=upcoming")
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof getAllUserBookings>>> =
|
console.log("asap responseBody", JSON.stringify(response.body, null, 2));
|
||||||
response.body;
|
const responseBody: GetBookingsOutput = response.body;
|
||||||
const fetchedBooking = responseBody.data.bookings[0];
|
const fetchedBooking = responseBody.data.bookings[0];
|
||||||
|
|
||||||
expect(responseBody.data.bookings.length).toEqual(1);
|
expect(responseBody.data.bookings.length).toEqual(1);
|
||||||
@@ -121,16 +153,16 @@ describe("Bookings Endpoints", () => {
|
|||||||
expect(fetchedBooking.uid).toEqual(createdBooking.uid);
|
expect(fetchedBooking.uid).toEqual(createdBooking.uid);
|
||||||
expect(fetchedBooking.startTime).toEqual(createdBooking.startTime);
|
expect(fetchedBooking.startTime).toEqual(createdBooking.startTime);
|
||||||
expect(fetchedBooking.endTime).toEqual(createdBooking.endTime);
|
expect(fetchedBooking.endTime).toEqual(createdBooking.endTime);
|
||||||
expect(fetchedBooking.user?.email).toEqual(createdBooking.user.email);
|
expect(fetchedBooking.user?.email).toEqual(userEmail);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get booking", async () => {
|
it("should get booking", async () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.get(`/api/v2/ee/bookings/${createdBooking.uid}`)
|
.get(`/v2/bookings/${createdBooking.uid}`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof getBookingInfo>>> = response.body;
|
const responseBody: GetBookingOutput = response.body;
|
||||||
const { bookingInfo } = responseBody.data;
|
const bookingInfo = responseBody.data;
|
||||||
|
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseBody.data).toBeDefined();
|
||||||
@@ -143,68 +175,71 @@ describe("Bookings Endpoints", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should create a recurring booking", async () => {
|
// note(Lauris) : found this test broken here - first thing to fix is that recurring endpoint accepts an array not 1 object.
|
||||||
const bookingStart = "2023-05-25T09:30:00.000Z";
|
// it("should create a recurring booking", async () => {
|
||||||
const bookingEnd = "2023-05-25T10:30:00.000Z";
|
// const bookingStart = "2040-05-25T09:30:00.000Z";
|
||||||
const bookingEventTypeId = 7;
|
// const bookingEnd = "2040-05-25T10:30:00.000Z";
|
||||||
const bookingTimeZone = "Europe/Londom";
|
// const bookingEventTypeId = 7;
|
||||||
const bookingLanguage = "en";
|
// const bookingTimeZone = "Europe/London";
|
||||||
const bookingHashedLink = "";
|
// const bookingLanguage = "en";
|
||||||
const bookingRecurringCount = 5;
|
// const bookingHashedLink = "";
|
||||||
const currentBookingRecurringIndex = 0;
|
// const bookingRecurringCount = 5;
|
||||||
|
// const currentBookingRecurringIndex = 0;
|
||||||
|
|
||||||
const body = {
|
// const body = {
|
||||||
start: bookingStart,
|
// start: bookingStart,
|
||||||
end: bookingEnd,
|
// end: bookingEnd,
|
||||||
eventTypeId: bookingEventTypeId,
|
// eventTypeId: bookingEventTypeId,
|
||||||
timeZone: bookingTimeZone,
|
// timeZone: bookingTimeZone,
|
||||||
language: bookingLanguage,
|
// language: bookingLanguage,
|
||||||
metadata: {},
|
// metadata: {},
|
||||||
hashedLink: bookingHashedLink,
|
// hashedLink: bookingHashedLink,
|
||||||
recurringCount: bookingRecurringCount,
|
// recurringCount: bookingRecurringCount,
|
||||||
currentRecurringIndex: currentBookingRecurringIndex,
|
// currentRecurringIndex: currentBookingRecurringIndex,
|
||||||
};
|
// };
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
// return request(app.getHttpServer())
|
||||||
.post("/api/v2/ee/bookings/reccuring")
|
// .post("/v2/bookings/recurring")
|
||||||
.send(body)
|
// .send(body)
|
||||||
.expect(201)
|
// .expect(201)
|
||||||
.then((response) => {
|
// .then((response) => {
|
||||||
const responseBody: ApiResponse<Awaited<ReturnType<typeof handleNewRecurringBooking>>> =
|
// const responseBody: ApiResponse<Awaited<ReturnType<typeof handleNewRecurringBooking>>> =
|
||||||
response.body;
|
// response.body;
|
||||||
|
|
||||||
expect(responseBody.status).toEqual("recurring");
|
// expect(responseBody.status).toEqual("recurring");
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
|
|
||||||
it("should create an instant booking", async () => {
|
// note(Lauris) : found this test broken here - first thing to fix is that the eventTypeId must be team event type, because
|
||||||
const bookingStart = "2023-05-25T09:30:00.000Z";
|
// instant bookings only work for teams.
|
||||||
const bookingEnd = "2023-05-25T10:30:00.000Z";
|
// it("should create an instant booking", async () => {
|
||||||
const bookingEventTypeId = 7;
|
// const bookingStart = "2040-05-25T09:30:00.000Z";
|
||||||
const bookingTimeZone = "Europe/Londom";
|
// const bookingEnd = "2040-25T10:30:00.000Z";
|
||||||
const bookingLanguage = "en";
|
// const bookingEventTypeId = 7;
|
||||||
const bookingHashedLink = "";
|
// const bookingTimeZone = "Europe/London";
|
||||||
|
// const bookingLanguage = "en";
|
||||||
|
// const bookingHashedLink = "";
|
||||||
|
|
||||||
const body = {
|
// const body = {
|
||||||
start: bookingStart,
|
// start: bookingStart,
|
||||||
end: bookingEnd,
|
// end: bookingEnd,
|
||||||
eventTypeId: bookingEventTypeId,
|
// eventTypeId: bookingEventTypeId,
|
||||||
timeZone: bookingTimeZone,
|
// timeZone: bookingTimeZone,
|
||||||
language: bookingLanguage,
|
// language: bookingLanguage,
|
||||||
metadata: {},
|
// metadata: {},
|
||||||
hashedLink: bookingHashedLink,
|
// hashedLink: bookingHashedLink,
|
||||||
};
|
// };
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
// return request(app.getHttpServer())
|
||||||
.post("/api/v2/ee/bookings/instant")
|
// .post("/v2/bookings/instant")
|
||||||
.send(body)
|
// .send(body)
|
||||||
.expect(201)
|
// .expect(201)
|
||||||
.then((response) => {
|
// .then((response) => {
|
||||||
const responseBody: ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>> = response.body;
|
// const responseBody: ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>> = response.body;
|
||||||
|
|
||||||
expect(responseBody.status).toEqual("instant");
|
// expect(responseBody.status).toEqual("instant");
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
|
|
||||||
it("should cancel a booking", async () => {
|
it("should cancel a booking", async () => {
|
||||||
const bookingId = createdBooking.id;
|
const bookingId = createdBooking.id;
|
||||||
@@ -215,7 +250,7 @@ describe("Bookings Endpoints", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post(`/api/v2/ee/bookings/${bookingId}/cancel`)
|
.post(`/v2/bookings/${bookingId}/cancel`)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(201)
|
.expect(201)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
||||||
import { CreateReccuringBookingInput } from "@/ee/bookings/inputs/create-reccuring-booking.input";
|
import { CreateRecurringBookingInput } from "@/ee/bookings/inputs/create-recurring-booking.input";
|
||||||
|
import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output";
|
||||||
|
import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output";
|
||||||
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 { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
import {
|
import {
|
||||||
@@ -13,6 +16,7 @@ import {
|
|||||||
Req,
|
Req,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
Body,
|
Body,
|
||||||
|
Headers,
|
||||||
HttpException,
|
HttpException,
|
||||||
Param,
|
Param,
|
||||||
Get,
|
Get,
|
||||||
@@ -20,11 +24,13 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
|
import { ApiQuery, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
import { NextApiRequest } from "next/types";
|
import { NextApiRequest } from "next/types";
|
||||||
|
|
||||||
import { BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
|
||||||
|
import { BOOKING_READ, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import {
|
import {
|
||||||
getAllUserBookings,
|
getAllUserBookings,
|
||||||
getBookingInfo,
|
getBookingInfo,
|
||||||
@@ -38,30 +44,57 @@ import {
|
|||||||
handleNewRecurringBooking,
|
handleNewRecurringBooking,
|
||||||
handleInstantMeeting,
|
handleInstantMeeting,
|
||||||
} from "@calcom/platform-libraries";
|
} from "@calcom/platform-libraries";
|
||||||
import { GetBookingsInput, CancelBookingInput } from "@calcom/platform-types";
|
import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-types";
|
||||||
import { ApiResponse } from "@calcom/platform-types";
|
import { ApiResponse } from "@calcom/platform-types";
|
||||||
import { PrismaClient } from "@calcom/prisma";
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
type BookingRequest = Request & {
|
||||||
|
userId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OAuthRequestParams = {
|
||||||
|
platformClientId: string;
|
||||||
|
platformRescheduleUrl: string;
|
||||||
|
platformCancelUrl: string;
|
||||||
|
platformBookingUrl: string;
|
||||||
|
platformBookingLocation?: string;
|
||||||
|
arePlatformEmailsEnabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_PLATFORM_PARAMS = {
|
||||||
|
platformClientId: "",
|
||||||
|
platformCancelUrl: "",
|
||||||
|
platformRescheduleUrl: "",
|
||||||
|
platformBookingUrl: "",
|
||||||
|
arePlatformEmailsEnabled: false,
|
||||||
|
platformBookingLocation: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "ee/bookings",
|
path: "/bookings",
|
||||||
version: "2",
|
version: "2",
|
||||||
})
|
})
|
||||||
@UseGuards(PermissionsGuard)
|
@UseGuards(PermissionsGuard)
|
||||||
|
@DocsTags("Bookings")
|
||||||
export class BookingsController {
|
export class BookingsController {
|
||||||
private readonly logger = new Logger("ee bookings controller");
|
private readonly logger = new Logger("ee bookings controller");
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly oAuthFlowService: OAuthFlowService,
|
private readonly oAuthFlowService: OAuthFlowService,
|
||||||
private readonly prismaReadService: PrismaReadService
|
private readonly prismaReadService: PrismaReadService,
|
||||||
|
private readonly oAuthClientRepository: OAuthClientRepository
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// note(Rajiv): currently this endpoint is atoms only
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(AccessTokenGuard)
|
||||||
|
@Permissions([BOOKING_READ])
|
||||||
|
@ApiQuery({ name: "filters[status]", enum: Status, required: true })
|
||||||
|
@ApiQuery({ name: "limit", type: "number", required: false })
|
||||||
|
@ApiQuery({ name: "cursor", type: "number", required: false })
|
||||||
async getBookings(
|
async getBookings(
|
||||||
@GetUser() user: User,
|
@GetUser() user: User,
|
||||||
@Query() queryParams: GetBookingsInput
|
@Query() queryParams: GetBookingsInput
|
||||||
): Promise<ApiResponse<unknown>> {
|
): Promise<GetBookingsOutput> {
|
||||||
const { filters, cursor, limit } = queryParams;
|
const { filters, cursor, limit } = queryParams;
|
||||||
const bookings = await getAllUserBookings({
|
const bookings = await getAllUserBookings({
|
||||||
bookingListingByStatus: filters.status,
|
bookingListingByStatus: filters.status,
|
||||||
@@ -80,9 +113,8 @@ export class BookingsController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// note(Rajiv): currently this endpoint is atoms only
|
|
||||||
@Get("/:bookingUid")
|
@Get("/:bookingUid")
|
||||||
async getBooking(@Param("bookingUid") bookingUid: string): Promise<ApiResponse<unknown>> {
|
async getBooking(@Param("bookingUid") bookingUid: string): Promise<GetBookingOutput> {
|
||||||
const { bookingInfo } = await getBookingInfo(bookingUid);
|
const { bookingInfo } = await getBookingInfo(bookingUid);
|
||||||
|
|
||||||
if (!bookingInfo) {
|
if (!bookingInfo) {
|
||||||
@@ -95,7 +127,6 @@ export class BookingsController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// note(Rajiv): currently this endpoint is atoms only
|
|
||||||
@Get("/:bookingUid/reschedule")
|
@Get("/:bookingUid/reschedule")
|
||||||
async getBookingForReschedule(@Param("bookingUid") bookingUid: string): Promise<ApiResponse<unknown>> {
|
async getBookingForReschedule(@Param("bookingUid") bookingUid: string): Promise<ApiResponse<unknown>> {
|
||||||
const booking = await getBookingForReschedule(bookingUid);
|
const booking = await getBookingForReschedule(bookingUid);
|
||||||
@@ -111,15 +142,17 @@ export class BookingsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@Permissions([BOOKING_WRITE])
|
|
||||||
async createBooking(
|
async createBooking(
|
||||||
@Req() req: Request & { userId?: number },
|
@Req() req: BookingRequest,
|
||||||
@Body() _: CreateBookingInput
|
@Body() body: CreateBookingInput,
|
||||||
|
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||||
): Promise<ApiResponse<unknown>> {
|
): Promise<ApiResponse<unknown>> {
|
||||||
req.userId = await this.getOwnerId(req);
|
const oAuthClientId = clientId?.toString();
|
||||||
req.body = { ...req.body, noEmail: true };
|
const locationUrl = body.locationUrl;
|
||||||
try {
|
try {
|
||||||
const booking = await handleNewBooking(req as unknown as NextApiRequest & { userId?: number });
|
const booking = await handleNewBooking(
|
||||||
|
await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl)
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: booking,
|
data: booking,
|
||||||
@@ -131,17 +164,17 @@ export class BookingsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("/:bookingId/cancel")
|
@Post("/:bookingId/cancel")
|
||||||
@Permissions([BOOKING_WRITE])
|
|
||||||
async cancelBooking(
|
async cancelBooking(
|
||||||
@Req() req: Request & { userId?: number },
|
@Req() req: BookingRequest,
|
||||||
@Param("bookingId") bookingId: string,
|
@Param("bookingId") bookingId: string,
|
||||||
@Body() body: CancelBookingInput
|
@Body() _: CancelBookingInput,
|
||||||
|
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||||
): Promise<ApiResponse> {
|
): Promise<ApiResponse> {
|
||||||
|
const oAuthClientId = clientId?.toString();
|
||||||
if (bookingId) {
|
if (bookingId) {
|
||||||
req.userId = await this.getOwnerId(req);
|
|
||||||
req.body = { ...body, id: parseInt(bookingId) };
|
|
||||||
try {
|
try {
|
||||||
await handleCancelBooking(req as unknown as NextApiRequest & { userId?: number });
|
req.body.id = parseInt(bookingId);
|
||||||
|
await handleCancelBooking(await this.createNextApiBookingRequest(req, oAuthClientId));
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
};
|
};
|
||||||
@@ -154,17 +187,16 @@ export class BookingsController {
|
|||||||
throw new InternalServerErrorException("Could not cancel booking.");
|
throw new InternalServerErrorException("Could not cancel booking.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("/reccuring")
|
@Post("/recurring")
|
||||||
@Permissions([BOOKING_WRITE])
|
async createRecurringBooking(
|
||||||
async createReccuringBooking(
|
@Req() req: BookingRequest,
|
||||||
@Req() req: Request & { userId?: number },
|
@Body() _: CreateRecurringBookingInput[],
|
||||||
@Body() _: CreateReccuringBookingInput[]
|
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||||
): Promise<ApiResponse<BookingResponse[]>> {
|
): Promise<ApiResponse<BookingResponse[]>> {
|
||||||
req.userId = await this.getOwnerId(req);
|
const oAuthClientId = clientId?.toString();
|
||||||
req.body = { ...req.body, noEmail: true };
|
|
||||||
try {
|
try {
|
||||||
const createdBookings: BookingResponse[] = await handleNewRecurringBooking(
|
const createdBookings: BookingResponse[] = await handleNewRecurringBooking(
|
||||||
req as unknown as NextApiRequest & { userId?: number }
|
await this.createNextApiBookingRequest(req, oAuthClientId)
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
@@ -177,16 +209,16 @@ export class BookingsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("/instant")
|
@Post("/instant")
|
||||||
@Permissions([BOOKING_WRITE])
|
|
||||||
async createInstantBooking(
|
async createInstantBooking(
|
||||||
@Req() req: Request & { userId?: number },
|
@Req() req: BookingRequest,
|
||||||
@Body() _: CreateBookingInput
|
@Body() _: CreateBookingInput,
|
||||||
|
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||||
): Promise<ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>>> {
|
): Promise<ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>>> {
|
||||||
req.userId = await this.getOwnerId(req);
|
const oAuthClientId = clientId?.toString();
|
||||||
req.body = { ...req.body, noEmail: true };
|
req.userId = (await this.getOwnerId(req)) ?? -1;
|
||||||
try {
|
try {
|
||||||
const instantMeeting = await handleInstantMeeting(
|
const instantMeeting = await handleInstantMeeting(
|
||||||
req as unknown as NextApiRequest & { userId?: number }
|
await this.createNextApiBookingRequest(req, oAuthClientId)
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
@@ -208,6 +240,39 @@ export class BookingsController {
|
|||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getOAuthClientsParams(req: BookingRequest, clientId: string): Promise<OAuthRequestParams> {
|
||||||
|
const res = DEFAULT_PLATFORM_PARAMS;
|
||||||
|
try {
|
||||||
|
const client = await this.oAuthClientRepository.getOAuthClient(clientId);
|
||||||
|
// fetch oAuthClient from db and use data stored in db to set these values
|
||||||
|
if (client) {
|
||||||
|
res.platformClientId = clientId;
|
||||||
|
res.platformCancelUrl = client.bookingCancelRedirectUri ?? "";
|
||||||
|
res.platformRescheduleUrl = client.bookingRescheduleRedirectUri ?? "";
|
||||||
|
res.platformBookingUrl = client.bookingRedirectUri ?? "";
|
||||||
|
res.arePlatformEmailsEnabled = client.areEmailsEnabled ?? false;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(err);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createNextApiBookingRequest(
|
||||||
|
req: BookingRequest,
|
||||||
|
oAuthClientId?: string,
|
||||||
|
platformBookingLocation?: string
|
||||||
|
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
|
||||||
|
const userId = (await this.getOwnerId(req)) ?? -1;
|
||||||
|
const oAuthParams = oAuthClientId
|
||||||
|
? await this.getOAuthClientsParams(req, oAuthClientId)
|
||||||
|
: DEFAULT_PLATFORM_PARAMS;
|
||||||
|
Object.assign(req, { userId, ...oAuthParams, platformBookingLocation });
|
||||||
|
req.body = { ...req.body, noEmail: !oAuthParams.arePlatformEmailsEnabled };
|
||||||
|
return req as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBookingErrors(err: Error | HttpError | unknown, type?: "recurring" | `instant`): void {
|
function handleBookingErrors(err: Error | HttpError | unknown, type?: "recurring" | `instant`): void {
|
||||||
|
|||||||
@@ -1,5 +1,44 @@
|
|||||||
import { Transform } from "class-transformer";
|
import { Transform, Type } from "class-transformer";
|
||||||
import { IsBoolean, IsTimeZone, IsNumber, IsString, IsOptional, IsArray } from "class-validator";
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsTimeZone,
|
||||||
|
IsNumber,
|
||||||
|
IsString,
|
||||||
|
IsOptional,
|
||||||
|
IsArray,
|
||||||
|
IsObject,
|
||||||
|
IsEmail,
|
||||||
|
ValidateNested,
|
||||||
|
} from "class-validator";
|
||||||
|
|
||||||
|
class Location {
|
||||||
|
@IsString()
|
||||||
|
optionValue!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
value!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Response {
|
||||||
|
@IsString()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
guests!: string[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => Location)
|
||||||
|
location?: Location;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class CreateBookingInput {
|
export class CreateBookingInput {
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -42,6 +81,7 @@ export class CreateBookingInput {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
bookingUid?: string;
|
bookingUid?: string;
|
||||||
|
|
||||||
|
@IsObject()
|
||||||
metadata!: Record<string, string>;
|
metadata!: Record<string, string>;
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@@ -55,4 +95,11 @@ export class CreateBookingInput {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
seatReferenceUid?: string;
|
seatReferenceUid?: string;
|
||||||
|
|
||||||
|
@Type(() => Response)
|
||||||
|
responses!: Response;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
locationUrl?: string;
|
||||||
}
|
}
|
||||||
+1
-1
@@ -3,7 +3,7 @@ import { IsBoolean, IsNumber, IsOptional } from "class-validator";
|
|||||||
|
|
||||||
import type { AppsStatus } from "@calcom/platform-libraries";
|
import type { AppsStatus } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
export class CreateReccuringBookingInput extends CreateBookingInput {
|
export class CreateRecurringBookingInput extends CreateBookingInput {
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
noEmail?: boolean;
|
noEmail?: boolean;
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsEnum,
|
||||||
|
IsInt,
|
||||||
|
IsOptional,
|
||||||
|
IsObject,
|
||||||
|
ValidateNested,
|
||||||
|
IsArray,
|
||||||
|
IsUrl,
|
||||||
|
IsDateString,
|
||||||
|
IsEmail,
|
||||||
|
} from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
class Metadata {
|
||||||
|
@IsUrl()
|
||||||
|
videoCallUrl!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Location {
|
||||||
|
@IsString()
|
||||||
|
optionValue!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
value!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Response {
|
||||||
|
@IsString()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
notes!: string;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
guests!: string[];
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => Location)
|
||||||
|
location!: Location;
|
||||||
|
}
|
||||||
|
|
||||||
|
class User {
|
||||||
|
@IsInt()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
name!: string | null;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
username!: string | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
timeZone!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Attendee {
|
||||||
|
@IsString()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
timeZone!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventType {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
eventName!: string | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
slug!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
timeZone!: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetBookingData {
|
||||||
|
@IsString()
|
||||||
|
title!: string;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
uid!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
description!: string | null;
|
||||||
|
|
||||||
|
@IsObject()
|
||||||
|
customInputs!: any;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
smsReminderNumber!: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
recurringEventId!: string | null;
|
||||||
|
|
||||||
|
@IsDateString()
|
||||||
|
startTime!: Date;
|
||||||
|
|
||||||
|
@IsDateString()
|
||||||
|
endTime!: Date;
|
||||||
|
|
||||||
|
@IsUrl()
|
||||||
|
location!: string | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
status!: string;
|
||||||
|
|
||||||
|
metadata!: Metadata | any;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
cancellationReason!: string | null;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => Response)
|
||||||
|
responses!: Response | any;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
rejectionReason!: string | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsEmail()
|
||||||
|
userPrimaryEmail!: string | null;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => User)
|
||||||
|
user!: User | null;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => Attendee)
|
||||||
|
@IsArray()
|
||||||
|
attendees!: Attendee[];
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
eventTypeId!: number | null;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => EventType)
|
||||||
|
eventType!: EventType | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetBookingOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: GetBookingData,
|
||||||
|
})
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => GetBookingData)
|
||||||
|
data!: GetBookingData;
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsEnum,
|
||||||
|
IsInt,
|
||||||
|
IsBoolean,
|
||||||
|
IsUrl,
|
||||||
|
IsOptional,
|
||||||
|
IsObject,
|
||||||
|
ValidateNested,
|
||||||
|
IsArray,
|
||||||
|
IsDateString,
|
||||||
|
IsEmail,
|
||||||
|
} from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
CANCELLED = "CANCELLED",
|
||||||
|
REJECTED = "REJECTED",
|
||||||
|
ACCEPTED = "ACCEPTED",
|
||||||
|
PENDING = "PENDING",
|
||||||
|
AWAITING_HOST = "AWAITING_HOST",
|
||||||
|
}
|
||||||
|
|
||||||
|
class Attendee {
|
||||||
|
@IsInt()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
timeZone!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
locale!: string | null;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
bookingId!: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventType {
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
slug?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
id?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
eventName?: string | null;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
price!: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
recurringEvent?: any;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
currency!: string;
|
||||||
|
|
||||||
|
@IsObject()
|
||||||
|
metadata!: any;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
seatsShowAttendees?: boolean | undefined | null;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
seatsShowAvailabilityCount?: boolean | undefined | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
team?: any | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Reference {
|
||||||
|
@IsInt()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
type!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
uid!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
meetingId?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
thirdPartyRecurringEventId?: string | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
meetingPassword!: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
meetingUrl?: string | null;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
bookingId!: number | null;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
externalCalendarId!: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
deleted?: any;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
credentialId!: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class User {
|
||||||
|
@IsInt()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
name!: string | null;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetBookingsDataEntry {
|
||||||
|
@IsInt()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
title!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail()
|
||||||
|
userPrimaryEmail?: string | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
description!: string | null;
|
||||||
|
|
||||||
|
@IsObject()
|
||||||
|
customInputs!: object | any;
|
||||||
|
|
||||||
|
@IsDateString()
|
||||||
|
startTime!: string;
|
||||||
|
|
||||||
|
@IsDateString()
|
||||||
|
endTime!: string;
|
||||||
|
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => Attendee)
|
||||||
|
@IsArray()
|
||||||
|
attendees!: Attendee[];
|
||||||
|
|
||||||
|
metadata!: any;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
uid!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
recurringEventId!: string | null;
|
||||||
|
|
||||||
|
@IsUrl()
|
||||||
|
location!: string | null;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => EventType)
|
||||||
|
eventType!: EventType;
|
||||||
|
|
||||||
|
@IsEnum(Status)
|
||||||
|
status!: "CANCELLED" | "REJECTED" | "ACCEPTED" | "PENDING" | "AWAITING_HOST";
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
paid!: boolean;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
payment!: any[];
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => Reference)
|
||||||
|
@IsArray()
|
||||||
|
references!: Reference[];
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
isRecorded!: boolean;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
seatsReferences!: any[];
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => User)
|
||||||
|
user!: User | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
rescheduled?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetBookingsData {
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => GetBookingsDataEntry)
|
||||||
|
@IsArray()
|
||||||
|
bookings!: GetBookingsDataEntry[];
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
recurringInfo!: any[];
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
nextCursor!: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetBookingsOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: GetBookingsData,
|
||||||
|
})
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => GetBookingsData)
|
||||||
|
data!: GetBookingsData;
|
||||||
|
}
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
|
import { GetBusyTimesOutput } from "@/ee/calendars/outputs/busy-times.output";
|
||||||
|
import { ConnectedCalendarsOutput } from "@/ee/calendars/outputs/connected-calendars.output";
|
||||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import { Controller, Get, UseGuards, Query } from "@nestjs/common";
|
import { Controller, Get, UseGuards, Query } from "@nestjs/common";
|
||||||
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { ConnectedDestinationCalendars } from "@calcom/platform-libraries";
|
|
||||||
import { CalendarBusyTimesInput } from "@calcom/platform-types";
|
import { CalendarBusyTimesInput } from "@calcom/platform-types";
|
||||||
import { ApiResponse } from "@calcom/platform-types";
|
|
||||||
import { EventBusyDate } from "@calcom/types/Calendar";
|
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "ee/calendars",
|
path: "/calendars",
|
||||||
version: "2",
|
version: "2",
|
||||||
})
|
})
|
||||||
@UseGuards(AccessTokenGuard)
|
@UseGuards(AccessTokenGuard)
|
||||||
|
@DocsTags("Calendars")
|
||||||
export class CalendarsController {
|
export class CalendarsController {
|
||||||
constructor(private readonly calendarsService: CalendarsService) {}
|
constructor(private readonly calendarsService: CalendarsService) {}
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ export class CalendarsController {
|
|||||||
async getBusyTimes(
|
async getBusyTimes(
|
||||||
@Query() queryParams: CalendarBusyTimesInput,
|
@Query() queryParams: CalendarBusyTimesInput,
|
||||||
@GetUser() user: UserWithProfile
|
@GetUser() user: UserWithProfile
|
||||||
): Promise<ApiResponse<EventBusyDate[]>> {
|
): Promise<GetBusyTimesOutput> {
|
||||||
const { loggedInUsersTz, dateFrom, dateTo, calendarsToLoad } = queryParams;
|
const { loggedInUsersTz, dateFrom, dateTo, calendarsToLoad } = queryParams;
|
||||||
if (!dateFrom || !dateTo) {
|
if (!dateFrom || !dateTo) {
|
||||||
return {
|
return {
|
||||||
@@ -46,7 +47,7 @@ export class CalendarsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
async getCalendars(@GetUser("id") userId: number): Promise<ApiResponse<ConnectedDestinationCalendars>> {
|
async getCalendars(@GetUser("id") userId: number): Promise<ConnectedCalendarsOutput> {
|
||||||
const calendars = await this.calendarsService.getCalendars(userId);
|
const calendars = await this.calendarsService.getCalendars(userId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsArray, IsDate, IsEnum, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class BusyTimesOutput {
|
||||||
|
@IsDate()
|
||||||
|
start!: Date;
|
||||||
|
|
||||||
|
@IsDate()
|
||||||
|
end!: Date;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
source?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetBusyTimesOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => BusyTimesOutput)
|
||||||
|
@IsArray()
|
||||||
|
data!: BusyTimesOutput[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsBoolean,
|
||||||
|
IsEmail,
|
||||||
|
IsEnum,
|
||||||
|
IsInt,
|
||||||
|
IsObject,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
IsUrl,
|
||||||
|
ValidateNested,
|
||||||
|
} from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
class Integration {
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
appData?: object | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
dirName?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
__template?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
description!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
installed?: boolean;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
type!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
variant!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
category?: string;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
categories!: string[];
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
logo!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
publisher!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
slug!: string;
|
||||||
|
|
||||||
|
@IsUrl()
|
||||||
|
url!: string;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
locationOption!: object | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Primary {
|
||||||
|
@IsEmail()
|
||||||
|
externalId!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
integration?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail()
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
primary!: boolean | null;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
readOnly!: boolean;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
@IsOptional()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
isSelected!: boolean;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
credentialId!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Calendar {
|
||||||
|
@IsEmail()
|
||||||
|
externalId!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
integration?: string;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
@IsOptional()
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
primary?: boolean | null;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
readOnly!: boolean;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
@IsOptional()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
isSelected!: boolean;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
credentialId!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectedCalendar {
|
||||||
|
@ValidateNested()
|
||||||
|
@IsObject()
|
||||||
|
integration!: Integration;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
credentialId!: number;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@IsObject()
|
||||||
|
@IsOptional()
|
||||||
|
primary?: Primary;
|
||||||
|
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@IsArray()
|
||||||
|
@IsOptional()
|
||||||
|
calendars?: Calendar[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class DestinationCalendar {
|
||||||
|
@IsInt()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
integration!: string;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
externalId!: string;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
primaryEmail!: string | null;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
userId!: number | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
eventTypeId!: number | null;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
credentialId!: number | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
name?: string | null;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
primary?: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
readOnly?: boolean;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
@IsOptional()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
integrationTitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectedCalendarsData {
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@IsArray()
|
||||||
|
connectedCalendars!: ConnectedCalendar[];
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@IsObject()
|
||||||
|
destinationCalendar!: DestinationCalendar;
|
||||||
|
}
|
||||||
|
export class ConnectedCalendarsOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => ConnectedCalendarsData)
|
||||||
|
data!: ConnectedCalendarsData;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
CredentialsWithUserEmail,
|
CredentialsWithUserEmail,
|
||||||
} from "@/modules/credentials/credentials.repository";
|
} from "@/modules/credentials/credentials.repository";
|
||||||
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 { UsersRepository } from "@/modules/users/users.repository";
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
import {
|
import {
|
||||||
Injectable,
|
Injectable,
|
||||||
@@ -23,7 +24,8 @@ export class CalendarsService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly usersRepository: UsersRepository,
|
private readonly usersRepository: UsersRepository,
|
||||||
private readonly credentialsRepository: CredentialsRepository,
|
private readonly credentialsRepository: CredentialsRepository,
|
||||||
private readonly dbRead: PrismaReadService
|
private readonly dbRead: PrismaReadService,
|
||||||
|
private readonly dbWrite: PrismaWriteService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getCalendars(userId: number) {
|
async getCalendars(userId: number) {
|
||||||
@@ -35,7 +37,7 @@ export class CalendarsService {
|
|||||||
return getConnectedDestinationCalendars(
|
return getConnectedDestinationCalendars(
|
||||||
userWithCalendars,
|
userWithCalendars,
|
||||||
false,
|
false,
|
||||||
this.dbRead.prisma as unknown as PrismaClient
|
this.dbWrite.prisma as unknown as PrismaClient
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { AppModule } from "@/app.module";
|
|||||||
import { EventTypesModule } from "@/ee/event-types/event-types.module";
|
import { EventTypesModule } from "@/ee/event-types/event-types.module";
|
||||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
||||||
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
||||||
|
import { GetEventTypePublicOutput } from "@/ee/event-types/outputs/get-event-type-public.output";
|
||||||
|
import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output";
|
||||||
|
import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-types-public.output";
|
||||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
@@ -63,7 +66,7 @@ describe("Event types Endpoints", () => {
|
|||||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||||
|
|
||||||
const userEmail = "event-types-test-e2e@api.com";
|
const userEmail = "event-types-test-e2e@api.com";
|
||||||
const name = "bob the builder";
|
const name = "bob-the-builder";
|
||||||
const username = name;
|
const username = name;
|
||||||
let eventType: EventType;
|
let eventType: EventType;
|
||||||
let user: User;
|
let user: User;
|
||||||
@@ -172,14 +175,49 @@ describe("Event types Endpoints", () => {
|
|||||||
.set("Authorization", `Bearer whatever`)
|
.set("Authorization", `Bearer whatever`)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const responseBody: ApiSuccessResponse<EventType> = response.body;
|
const responseBody: GetEventTypeOutput = response.body;
|
||||||
|
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseBody.data).toBeDefined();
|
||||||
expect(responseBody.data.id).toEqual(eventType.id);
|
expect(responseBody.data.eventType.id).toEqual(eventType.id);
|
||||||
expect(responseBody.data.title).toEqual(eventType.title);
|
expect(responseBody.data.eventType.title).toEqual(eventType.title);
|
||||||
expect(responseBody.data.slug).toEqual(eventType.slug);
|
expect(responseBody.data.eventType.slug).toEqual(eventType.slug);
|
||||||
expect(responseBody.data.userId).toEqual(user.id);
|
expect(responseBody.data.eventType.userId).toEqual(user.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/:username/public`, async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/api/v2/event-types/${username}/public`)
|
||||||
|
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||||
|
.set("Authorization", `Bearer whatever`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: GetEventTypesPublicOutput = response.body;
|
||||||
|
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.data?.length).toEqual(1);
|
||||||
|
expect(responseBody.data?.[0]?.id).toEqual(eventType.id);
|
||||||
|
expect(responseBody.data?.[0]?.title).toEqual(eventType.title);
|
||||||
|
expect(responseBody.data?.[0]?.slug).toEqual(eventType.slug);
|
||||||
|
expect(responseBody.data?.[0]?.length).toEqual(eventType.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`/GET/:username/:eventSlug/public`, async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get(`/api/v2/event-types/${username}/${eventType.slug}/public`)
|
||||||
|
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||||
|
.set("Authorization", `Bearer whatever`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseBody: GetEventTypePublicOutput = response.body;
|
||||||
|
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(responseBody.data?.id).toEqual(eventType.id);
|
||||||
|
expect(responseBody.data?.title).toEqual(eventType.title);
|
||||||
|
expect(responseBody.data?.slug).toEqual(eventType.slug);
|
||||||
|
expect(responseBody.data?.length).toEqual(eventType.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/GET/`, async () => {
|
it(`/GET/`, async () => {
|
||||||
@@ -201,7 +239,7 @@ describe("Event types Endpoints", () => {
|
|||||||
expect(responseBody.data.profiles?.[0]?.name).toEqual(name);
|
expect(responseBody.data.profiles?.[0]?.name).toEqual(name);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/GET/:username/public`, async () => {
|
it(`/GET/public/:username/`, async () => {
|
||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.get(`/api/v2/event-types/${username}/public`)
|
.get(`/api/v2/event-types/${username}/public`)
|
||||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
||||||
|
import { GetPublicEventTypeQueryParams } from "@/ee/event-types/inputs/get-public-event-type-query-params.input";
|
||||||
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
||||||
import { CreateEventTypeOutput } from "@/ee/event-types/outputs/create-event-type.output";
|
import { CreateEventTypeOutput } from "@/ee/event-types/outputs/create-event-type.output";
|
||||||
import { DeleteEventTypeOutput } from "@/ee/event-types/outputs/delete-event-type.output";
|
import { DeleteEventTypeOutput } from "@/ee/event-types/outputs/delete-event-type.output";
|
||||||
|
import { GetEventTypePublicOutput } from "@/ee/event-types/outputs/get-event-type-public.output";
|
||||||
import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output";
|
import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output";
|
||||||
import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-types-public.output";
|
import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-types-public.output";
|
||||||
import { GetEventTypesOutput } from "@/ee/event-types/outputs/get-event-types.output";
|
import { GetEventTypesOutput } from "@/ee/event-types/outputs/get-event-types.output";
|
||||||
@@ -11,6 +13,7 @@ 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 { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
@@ -24,11 +27,15 @@ import {
|
|||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Delete,
|
Delete,
|
||||||
|
Query,
|
||||||
|
InternalServerErrorException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { getPublicEvent } from "@calcom/platform-libraries";
|
||||||
import { getEventTypesByViewer } from "@calcom/platform-libraries";
|
import { getEventTypesByViewer } from "@calcom/platform-libraries";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "event-types",
|
path: "event-types",
|
||||||
@@ -37,7 +44,10 @@ import { getEventTypesByViewer } from "@calcom/platform-libraries";
|
|||||||
@UseGuards(PermissionsGuard)
|
@UseGuards(PermissionsGuard)
|
||||||
@DocsTags("Event types")
|
@DocsTags("Event types")
|
||||||
export class EventTypesController {
|
export class EventTypesController {
|
||||||
constructor(private readonly eventTypesService: EventTypesService) {}
|
constructor(
|
||||||
|
private readonly eventTypesService: EventTypesService,
|
||||||
|
private readonly prismaReadService: PrismaReadService
|
||||||
|
) {}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@Permissions([EVENT_TYPE_WRITE])
|
@Permissions([EVENT_TYPE_WRITE])
|
||||||
@@ -90,8 +100,36 @@ export class EventTypesController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("/:username/:eventSlug/public")
|
||||||
|
async getPublicEventType(
|
||||||
|
@Param("username") username: string,
|
||||||
|
@Param("eventSlug") eventSlug: string,
|
||||||
|
@Query() queryParams: GetPublicEventTypeQueryParams
|
||||||
|
): Promise<GetEventTypePublicOutput> {
|
||||||
|
try {
|
||||||
|
const event = await getPublicEvent(
|
||||||
|
username.toLowerCase(),
|
||||||
|
eventSlug,
|
||||||
|
queryParams.isTeamEvent,
|
||||||
|
queryParams.org || null,
|
||||||
|
this.prismaReadService.prisma as unknown as PrismaClient,
|
||||||
|
// We should be fine allowing unpublished orgs events to be servable through platform because Platform access is behind license
|
||||||
|
// If there is ever a need to restrict this, we can introduce a new query param `fromRedirectOfNonOrgLink`
|
||||||
|
true
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
data: event,
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
throw new NotFoundException(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new InternalServerErrorException("Could not find public event.");
|
||||||
|
}
|
||||||
|
|
||||||
@Get("/:username/public")
|
@Get("/:username/public")
|
||||||
@Permissions([EVENT_TYPE_READ])
|
|
||||||
async getPublicEventTypes(@Param("username") username: string): Promise<GetEventTypesPublicOutput> {
|
async getPublicEventTypes(@Param("username") username: string): Promise<GetEventTypesPublicOutput> {
|
||||||
const eventTypes = await this.eventTypesService.getEventTypesPublicByUsername(username);
|
const eventTypes = await this.eventTypesService.getEventTypesPublicByUsername(username);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
import { getEventTypeById } from "@calcom/platform-libraries";
|
import { getEventTypeById } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
@@ -44,19 +45,14 @@ export class EventTypesRepository {
|
|||||||
isUserOrganizationAdmin: boolean,
|
isUserOrganizationAdmin: boolean,
|
||||||
eventTypeId: number
|
eventTypeId: number
|
||||||
) {
|
) {
|
||||||
try {
|
return await getEventTypeById({
|
||||||
return getEventTypeById({
|
currentOrganizationId: user.movedToProfile?.organizationId || user.organizationId,
|
||||||
currentOrganizationId: user.movedToProfile?.organizationId || user.organizationId,
|
eventTypeId,
|
||||||
eventTypeId,
|
userId: user.id,
|
||||||
userId: user.id,
|
prisma: this.dbRead.prisma as unknown as PrismaClient,
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
isUserOrganizationAdmin,
|
||||||
// @ts-ignore
|
isTrpcCall: true,
|
||||||
prisma: this.dbRead.prisma,
|
});
|
||||||
isUserOrganizationAdmin,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
throw new NotFoundException(`User with id ${user.id} has no event type with id ${eventTypeId}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEventTypeById(eventTypeId: number) {
|
async getEventTypeById(eventTypeId: number) {
|
||||||
|
|||||||
+1
-10
@@ -2,16 +2,7 @@ import { ApiProperty } from "@nestjs/swagger";
|
|||||||
import { Transform } from "class-transformer";
|
import { Transform } from "class-transformer";
|
||||||
import { IsBoolean, IsOptional, IsString } from "class-validator";
|
import { IsBoolean, IsOptional, IsString } from "class-validator";
|
||||||
|
|
||||||
export class GetPublicEventInput {
|
export class GetPublicEventTypeQueryParams {
|
||||||
@IsString()
|
|
||||||
@Transform(({ value }: { value: string }) => value.toLowerCase())
|
|
||||||
@ApiProperty({ required: true })
|
|
||||||
username!: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@ApiProperty({ required: true })
|
|
||||||
eventSlug!: string;
|
|
||||||
|
|
||||||
@Transform(({ value }: { value: string }) => value === "true")
|
@Transform(({ value }: { value: string }) => value === "true")
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -55,7 +55,7 @@ export class EventTypeOutput {
|
|||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => EventTypeLocation)
|
@Type(() => EventTypeLocation)
|
||||||
@IsArray()
|
@IsArray()
|
||||||
locations!: EventTypeLocation | null;
|
locations!: EventTypeLocation[] | null;
|
||||||
|
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@ApiHideProperty()
|
@ApiHideProperty()
|
||||||
|
|||||||
@@ -0,0 +1,355 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsInt,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
IsUrl,
|
||||||
|
ValidateNested,
|
||||||
|
IsArray,
|
||||||
|
IsObject,
|
||||||
|
IsNumber,
|
||||||
|
IsEnum,
|
||||||
|
} from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
class Location {
|
||||||
|
@IsString()
|
||||||
|
type!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Source {
|
||||||
|
@IsString()
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
type!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
label!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class OptionInput {
|
||||||
|
@IsString()
|
||||||
|
type!: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
required?: boolean;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class BookingField {
|
||||||
|
@IsString()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
type!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
defaultLabel?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
label?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
placeholder?: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
required?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
getOptionsAt?: string;
|
||||||
|
|
||||||
|
@IsObject()
|
||||||
|
@IsOptional()
|
||||||
|
optionsInputs?: { [key: string]: OptionInput };
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
hideWhenJustOneOption?: boolean;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
editable?: string;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => Source)
|
||||||
|
@IsOptional()
|
||||||
|
sources?: Source[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class Organization {
|
||||||
|
@IsInt()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
slug?: string | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
metadata!: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Profile {
|
||||||
|
@IsString()
|
||||||
|
username!: string | null;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
id!: number | null;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@IsOptional()
|
||||||
|
userId?: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
uid?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
organizationId!: number | null;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => Organization)
|
||||||
|
organization?: Organization | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
upId!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
image?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
brandColor?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
darkBrandColor?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
theme?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
bookerLayouts?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Owner {
|
||||||
|
@IsInt()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
avatarUrl?: string | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
username!: string | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
name!: string | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
weekStart!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
brandColor?: string | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
darkBrandColor?: string | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
theme?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
metadata!: any;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@IsOptional()
|
||||||
|
defaultScheduleId?: number | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
nonProfileUsername!: string | null;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => Profile)
|
||||||
|
profile!: Profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
class User {
|
||||||
|
@IsString()
|
||||||
|
username!: string | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
name!: string | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
weekStart!: string;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
organizationId?: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
avatarUrl?: string | null;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
profile!: Profile;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
bookerUrl!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Schedule {
|
||||||
|
@IsInt()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
timeZone!: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PublicEventTypeOutput {
|
||||||
|
@IsInt()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
title!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
description!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
eventName?: string | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
slug!: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
isInstantEvent!: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
aiPhoneCallConfig?: any;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
schedulingType?: any;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
length!: number;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => Location)
|
||||||
|
locations!: Location[];
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
customInputs!: any[];
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
disableGuests!: boolean;
|
||||||
|
|
||||||
|
@IsObject()
|
||||||
|
metadata!: object | null;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
lockTimeZoneToggleOnBookingPage!: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
requiresConfirmation!: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
requiresBookerEmailVerification!: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
recurringEvent?: any;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
price!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
currency!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
seatsPerTimeSlot?: number | null;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
seatsShowAvailabilityCount!: boolean | null;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => BookingField)
|
||||||
|
bookingFields!: BookingField[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
team?: any;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUrl()
|
||||||
|
successRedirectUrl?: string | null;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
workflows!: any[];
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
hosts!: any[];
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => Owner)
|
||||||
|
owner!: Owner | null;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => Schedule)
|
||||||
|
schedule!: Schedule | null;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
hidden!: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
assignAllTeamMembers!: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
bookerLayouts?: any;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => User)
|
||||||
|
users!: User[];
|
||||||
|
|
||||||
|
@IsObject()
|
||||||
|
entity!: object;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
isDynamic!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetEventTypePublicOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => PublicEventTypeOutput)
|
||||||
|
@IsArray()
|
||||||
|
data!: PublicEventTypeOutput | null;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { bootstrap } from "@/app";
|
import { bootstrap } from "@/app";
|
||||||
import { AppModule } from "@/app.module";
|
import { AppModule } from "@/app.module";
|
||||||
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
@@ -18,6 +19,20 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
|
|||||||
|
|
||||||
const CLIENT_REDIRECT_URI = "http://localhost:5555";
|
const CLIENT_REDIRECT_URI = "http://localhost:5555";
|
||||||
|
|
||||||
|
class CalendarsServiceMock {
|
||||||
|
async getCalendars() {
|
||||||
|
return {
|
||||||
|
connectedCalendars: [
|
||||||
|
{
|
||||||
|
integration: {
|
||||||
|
type: "google_calendar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe("Platform Gcal Endpoints", () => {
|
describe("Platform Gcal Endpoints", () => {
|
||||||
let app: INestApplication;
|
let app: INestApplication;
|
||||||
|
|
||||||
@@ -42,6 +57,8 @@ describe("Platform Gcal Endpoints", () => {
|
|||||||
.useValue({
|
.useValue({
|
||||||
canActivate: () => true,
|
canActivate: () => true,
|
||||||
})
|
})
|
||||||
|
.overrideProvider(CalendarsService)
|
||||||
|
.useClass(CalendarsServiceMock)
|
||||||
.compile();
|
.compile();
|
||||||
|
|
||||||
app = moduleRef.createNestApplication();
|
app = moduleRef.createNestApplication();
|
||||||
@@ -85,14 +102,14 @@ describe("Platform Gcal Endpoints", () => {
|
|||||||
|
|
||||||
it(`/GET/ee/gcal/oauth/auth-url: it should respond 401 with invalid access token`, async () => {
|
it(`/GET/ee/gcal/oauth/auth-url: it should respond 401 with invalid access token`, async () => {
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.get(`/api/v2/ee/gcal/oauth/auth-url`)
|
.get(`/v2/gcal/oauth/auth-url`)
|
||||||
.set("Authorization", `Bearer invalid_access_token`)
|
.set("Authorization", `Bearer invalid_access_token`)
|
||||||
.expect(401);
|
.expect(401);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/GET/ee/gcal/oauth/auth-url: it should auth-url to google oauth with valid access token `, async () => {
|
it(`/GET/ee/gcal/oauth/auth-url: it should auth-url to google oauth with valid access token `, async () => {
|
||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.get(`/api/v2/ee/gcal/oauth/auth-url`)
|
.get(`/v2/gcal/oauth/auth-url`)
|
||||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
.set("Origin", CLIENT_REDIRECT_URI)
|
.set("Origin", CLIENT_REDIRECT_URI)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
@@ -103,7 +120,7 @@ describe("Platform Gcal Endpoints", () => {
|
|||||||
it(`/GET/ee/gcal/oauth/save: without oauth code`, async () => {
|
it(`/GET/ee/gcal/oauth/save: without oauth code`, async () => {
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.get(
|
.get(
|
||||||
`/api/v2/ee/gcal/oauth/save?state=accessToken=${accessTokenSecret}&origin%3D${CLIENT_REDIRECT_URI}&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
`/v2/gcal/oauth/save?state=accessToken=${accessTokenSecret}&origin%3D${CLIENT_REDIRECT_URI}&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||||
)
|
)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
||||||
@@ -111,7 +128,7 @@ describe("Platform Gcal Endpoints", () => {
|
|||||||
it(`/GET/ee/gcal/oauth/save: without access token`, async () => {
|
it(`/GET/ee/gcal/oauth/save: without access token`, async () => {
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.get(
|
.get(
|
||||||
`/api/v2/ee/gcal/oauth/save?state=origin%3D${CLIENT_REDIRECT_URI}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
`/v2/gcal/oauth/save?state=origin%3D${CLIENT_REDIRECT_URI}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||||
)
|
)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
||||||
@@ -119,25 +136,25 @@ describe("Platform Gcal Endpoints", () => {
|
|||||||
it(`/GET/ee/gcal/oauth/save: without origin`, async () => {
|
it(`/GET/ee/gcal/oauth/save: without origin`, async () => {
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.get(
|
.get(
|
||||||
`/api/v2/ee/gcal/oauth/save?state=accessToken=${accessTokenSecret}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
`/v2/gcal/oauth/save?state=accessToken=${accessTokenSecret}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||||
)
|
)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/GET/ee/gcal/check with access token but without origin`, async () => {
|
it(`/GET/ee/gcal/check with access token but without origin`, async () => {
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.get(`/api/v2/ee/gcal/check`)
|
.get(`/v2/gcal/check`)
|
||||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/GET/ee/gcal/check without access token`, async () => {
|
it(`/GET/ee/gcal/check without access token`, async () => {
|
||||||
await request(app.getHttpServer()).get(`/api/v2/ee/gcal/check`).expect(401);
|
await request(app.getHttpServer()).get(`/v2/gcal/check`).expect(401);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/GET/ee/gcal/check with access token and origin but no credentials`, async () => {
|
it(`/GET/ee/gcal/check with access token and origin but no credentials`, async () => {
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.get(`/api/v2/ee/gcal/check`)
|
.get(`/v2/gcal/check`)
|
||||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
.set("Origin", CLIENT_REDIRECT_URI)
|
.set("Origin", CLIENT_REDIRECT_URI)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
@@ -151,7 +168,7 @@ describe("Platform Gcal Endpoints", () => {
|
|||||||
"google-calendar"
|
"google-calendar"
|
||||||
);
|
);
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.get(`/api/v2/ee/gcal/check`)
|
.get(`/v2/gcal/check`)
|
||||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||||
.set("Origin", CLIENT_REDIRECT_URI)
|
.set("Origin", CLIENT_REDIRECT_URI)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
|
import { GcalAuthUrlOutput } from "@/ee/gcal/outputs/auth-url.output";
|
||||||
|
import { GcalCheckOutput } from "@/ee/gcal/outputs/check.output";
|
||||||
|
import { GcalSaveRedirectOutput } from "@/ee/gcal/outputs/save-redirect.output";
|
||||||
import { GCalService } from "@/modules/apps/services/gcal.service";
|
import { GCalService } from "@/modules/apps/services/gcal.service";
|
||||||
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";
|
||||||
@@ -21,6 +25,7 @@ import {
|
|||||||
Headers,
|
Headers,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
import { google } from "googleapis";
|
import { google } from "googleapis";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -31,17 +36,18 @@ import {
|
|||||||
GOOGLE_CALENDAR_TYPE,
|
GOOGLE_CALENDAR_TYPE,
|
||||||
SUCCESS_STATUS,
|
SUCCESS_STATUS,
|
||||||
} from "@calcom/platform-constants";
|
} from "@calcom/platform-constants";
|
||||||
import { ApiRedirectResponseType, ApiResponse } from "@calcom/platform-types";
|
|
||||||
|
|
||||||
const CALENDAR_SCOPES = [
|
const CALENDAR_SCOPES = [
|
||||||
"https://www.googleapis.com/auth/calendar.readonly",
|
"https://www.googleapis.com/auth/calendar.readonly",
|
||||||
"https://www.googleapis.com/auth/calendar.events",
|
"https://www.googleapis.com/auth/calendar.events",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Controller for the GCalConnect Atom
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "ee/gcal",
|
path: "/gcal",
|
||||||
version: "2",
|
version: "2",
|
||||||
})
|
})
|
||||||
|
@DocsTags("Google Calendar")
|
||||||
export class GcalController {
|
export class GcalController {
|
||||||
private readonly logger = new Logger("Platform Gcal Provider");
|
private readonly logger = new Logger("Platform Gcal Provider");
|
||||||
|
|
||||||
@@ -50,10 +56,11 @@ export class GcalController {
|
|||||||
private readonly tokensRepository: TokensRepository,
|
private readonly tokensRepository: TokensRepository,
|
||||||
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
private readonly gcalService: GCalService
|
private readonly gcalService: GCalService,
|
||||||
|
private readonly calendarsService: CalendarsService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private redirectUri = `${this.config.get("api.url")}/ee/gcal/oauth/save`;
|
private redirectUri = `${this.config.get("api.url")}/gcal/oauth/save`;
|
||||||
|
|
||||||
@Get("/oauth/auth-url")
|
@Get("/oauth/auth-url")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@@ -61,7 +68,7 @@ export class GcalController {
|
|||||||
async redirect(
|
async redirect(
|
||||||
@Headers("Authorization") authorization: string,
|
@Headers("Authorization") authorization: string,
|
||||||
@Req() req: Request
|
@Req() req: Request
|
||||||
): Promise<ApiResponse<{ authUrl: string }>> {
|
): Promise<GcalAuthUrlOutput> {
|
||||||
const oAuth2Client = await this.gcalService.getOAuthClient(this.redirectUri);
|
const oAuth2Client = await this.gcalService.getOAuthClient(this.redirectUri);
|
||||||
const accessToken = authorization.replace("Bearer ", "");
|
const accessToken = authorization.replace("Bearer ", "");
|
||||||
const origin = req.get("origin") ?? req.get("host");
|
const origin = req.get("origin") ?? req.get("host");
|
||||||
@@ -77,7 +84,7 @@ export class GcalController {
|
|||||||
@Get("/oauth/save")
|
@Get("/oauth/save")
|
||||||
@Redirect(undefined, 301)
|
@Redirect(undefined, 301)
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async save(@Query("state") state: string, @Query("code") code: string): Promise<ApiRedirectResponseType> {
|
async save(@Query("state") state: string, @Query("code") code: string): Promise<GcalSaveRedirectOutput> {
|
||||||
const stateParams = new URLSearchParams(state);
|
const stateParams = new URLSearchParams(state);
|
||||||
const { accessToken, origin } = z
|
const { accessToken, origin } = z
|
||||||
.object({ accessToken: z.string(), origin: z.string() })
|
.object({ accessToken: z.string(), origin: z.string() })
|
||||||
@@ -133,7 +140,7 @@ export class GcalController {
|
|||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
||||||
@Permissions([APPS_READ])
|
@Permissions([APPS_READ])
|
||||||
async check(@GetUser("id") userId: number): Promise<ApiResponse> {
|
async check(@GetUser("id") userId: number): Promise<GcalCheckOutput> {
|
||||||
const gcalCredentials = await this.credentialRepository.getByTypeAndUserId("google_calendar", userId);
|
const gcalCredentials = await this.credentialRepository.getByTypeAndUserId("google_calendar", userId);
|
||||||
|
|
||||||
if (!gcalCredentials) {
|
if (!gcalCredentials) {
|
||||||
@@ -144,6 +151,17 @@ export class GcalController {
|
|||||||
throw new BadRequestException("Invalid google oauth credentials.");
|
throw new BadRequestException("Invalid google oauth credentials.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { connectedCalendars } = await this.calendarsService.getCalendars(userId);
|
||||||
|
const googleCalendar = connectedCalendars.find(
|
||||||
|
(cal: { integration: { type: string } }) => cal.integration.type === GOOGLE_CALENDAR_TYPE
|
||||||
|
);
|
||||||
|
if (!googleCalendar) {
|
||||||
|
throw new UnauthorizedException("Google Calendar not connected.");
|
||||||
|
}
|
||||||
|
if (googleCalendar.error?.message) {
|
||||||
|
throw new UnauthorizedException(googleCalendar.error?.message);
|
||||||
|
}
|
||||||
|
|
||||||
return { status: SUCCESS_STATUS };
|
return { status: SUCCESS_STATUS };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
import { GcalController } from "@/ee/gcal/gcal.controller";
|
import { GcalController } from "@/ee/gcal/gcal.controller";
|
||||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
import { GCalService } from "@/modules/apps/services/gcal.service";
|
import { GCalService } from "@/modules/apps/services/gcal.service";
|
||||||
@@ -6,12 +7,21 @@ import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
|||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, TokensModule, OAuthClientModule],
|
imports: [PrismaModule, TokensModule, OAuthClientModule],
|
||||||
providers: [AppsRepository, ConfigService, CredentialsRepository, SelectedCalendarsRepository, GCalService],
|
providers: [
|
||||||
|
AppsRepository,
|
||||||
|
ConfigService,
|
||||||
|
CredentialsRepository,
|
||||||
|
SelectedCalendarsRepository,
|
||||||
|
GCalService,
|
||||||
|
CalendarsService,
|
||||||
|
UsersRepository,
|
||||||
|
],
|
||||||
controllers: [GcalController],
|
controllers: [GcalController],
|
||||||
})
|
})
|
||||||
export class GcalModule {}
|
export class GcalModule {}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsEnum, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
class AuthUrlData {
|
||||||
|
@IsString()
|
||||||
|
authUrl!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GcalAuthUrlOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@Type(() => AuthUrlData)
|
||||||
|
@ValidateNested()
|
||||||
|
data!: AuthUrlData;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsEnum } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class GcalCheckOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class GcalSaveRedirectOutput {
|
||||||
|
@IsString()
|
||||||
|
url!: string;
|
||||||
|
}
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import { bootstrap } from "@/app";
|
import { bootstrap } from "@/app";
|
||||||
import { AppModule } from "@/app.module";
|
import { AppModule } from "@/app.module";
|
||||||
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
import { SchedulesModule } from "@/ee/schedules/schedules.module";
|
||||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
import { UpdateManagedPlatformUserInput } from "@/modules/users/inputs/update-managed-platform-user.input";
|
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
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";
|
||||||
@@ -35,8 +34,14 @@ describe("Me Endpoints", () => {
|
|||||||
const moduleRef = await withAccessTokenAuth(
|
const moduleRef = await withAccessTokenAuth(
|
||||||
userEmail,
|
userEmail,
|
||||||
Test.createTestingModule({
|
Test.createTestingModule({
|
||||||
imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule, TokensModule],
|
imports: [
|
||||||
providers: [SchedulesRepository, SchedulesService],
|
AppModule,
|
||||||
|
PrismaModule,
|
||||||
|
AvailabilitiesModule,
|
||||||
|
UsersModule,
|
||||||
|
TokensModule,
|
||||||
|
SchedulesModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.overrideGuard(PermissionsGuard)
|
.overrideGuard(PermissionsGuard)
|
||||||
@@ -50,6 +55,7 @@ describe("Me Endpoints", () => {
|
|||||||
|
|
||||||
user = await userRepositoryFixture.create({
|
user = await userRepositoryFixture.create({
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
|
username: userEmail,
|
||||||
});
|
});
|
||||||
|
|
||||||
app = moduleRef.createNestApplication();
|
app = moduleRef.createNestApplication();
|
||||||
@@ -65,7 +71,7 @@ describe("Me Endpoints", () => {
|
|||||||
|
|
||||||
it("should get user associated with access token", async () => {
|
it("should get user associated with access token", async () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.get("/api/v2/ee/me")
|
.get("/v2/me")
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const responseBody: ApiSuccessResponse<UserResponse> = response.body;
|
const responseBody: ApiSuccessResponse<UserResponse> = response.body;
|
||||||
@@ -81,10 +87,10 @@ describe("Me Endpoints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should update user associated with access token", async () => {
|
it("should update user associated with access token", async () => {
|
||||||
const body: UpdateManagedPlatformUserInput = { timeZone: "Europe/Rome" };
|
const body: UpdateManagedUserInput = { timeZone: "Europe/Rome" };
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.patch("/api/v2/ee/me")
|
.patch("/v2/me")
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
@@ -106,24 +112,21 @@ describe("Me Endpoints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not update user associated with access token given invalid timezone", async () => {
|
it("should not update user associated with access token given invalid timezone", async () => {
|
||||||
const bodyWithIncorrectTimeZone: UpdateManagedPlatformUserInput = { timeZone: "Narnia/Woods" };
|
const bodyWithIncorrectTimeZone: UpdateManagedUserInput = { timeZone: "Narnia/Woods" };
|
||||||
|
|
||||||
return request(app.getHttpServer()).patch("/api/v2/ee/me").send(bodyWithIncorrectTimeZone).expect(400);
|
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectTimeZone).expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not update user associated with access token given invalid time format", async () => {
|
it("should not update user associated with access token given invalid time format", async () => {
|
||||||
const bodyWithIncorrectTimeFormat: UpdateManagedPlatformUserInput = { timeFormat: 100 };
|
const bodyWithIncorrectTimeFormat: UpdateManagedUserInput = { timeFormat: 100 };
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectTimeFormat).expect(400);
|
||||||
.patch("/api/v2/ee/me")
|
|
||||||
.send(bodyWithIncorrectTimeFormat)
|
|
||||||
.expect(400);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not update user associated with access token given invalid week start", async () => {
|
it("should not update user associated with access token given invalid week start", async () => {
|
||||||
const bodyWithIncorrectWeekStart: UpdateManagedPlatformUserInput = { weekStart: "waba luba dub dub" };
|
const bodyWithIncorrectWeekStart: UpdateManagedUserInput = { weekStart: "waba luba dub dub" };
|
||||||
|
|
||||||
return request(app.getHttpServer()).patch("/api/v2/ee/me").send(bodyWithIncorrectWeekStart).expect(400);
|
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectWeekStart).expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
|
import { GetMeOutput } from "@/ee/me/outputs/get-me.output";
|
||||||
|
import { UpdateMeOutput } from "@/ee/me/outputs/update-me.output";
|
||||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
||||||
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 { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { UpdateManagedPlatformUserInput } from "@/modules/users/inputs/update-managed-platform-user.input";
|
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { Controller, UseGuards, Get, Patch, Body } from "@nestjs/common";
|
import { Controller, UseGuards, Get, Patch, Body } from "@nestjs/common";
|
||||||
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { PROFILE_READ, PROFILE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { PROFILE_READ, PROFILE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { UserResponse, userSchemaResponse } from "@calcom/platform-types";
|
import { userSchemaResponse } from "@calcom/platform-types";
|
||||||
import { ApiResponse } from "@calcom/platform-types";
|
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "ee/me",
|
path: "/me",
|
||||||
version: "2",
|
version: "2",
|
||||||
})
|
})
|
||||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
||||||
|
@DocsTags("Me")
|
||||||
export class MeController {
|
export class MeController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly usersRepository: UsersRepository,
|
private readonly usersRepository: UsersRepository,
|
||||||
@@ -24,7 +27,7 @@ export class MeController {
|
|||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@Permissions([PROFILE_READ])
|
@Permissions([PROFILE_READ])
|
||||||
async getMe(@GetUser() user: UserWithProfile): Promise<ApiResponse<UserResponse>> {
|
async getMe(@GetUser() user: UserWithProfile): Promise<GetMeOutput> {
|
||||||
const me = userSchemaResponse.parse(user);
|
const me = userSchemaResponse.parse(user);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -37,11 +40,11 @@ export class MeController {
|
|||||||
@Permissions([PROFILE_WRITE])
|
@Permissions([PROFILE_WRITE])
|
||||||
async updateMe(
|
async updateMe(
|
||||||
@GetUser() user: UserWithProfile,
|
@GetUser() user: UserWithProfile,
|
||||||
@Body() bodySchedule: UpdateManagedPlatformUserInput
|
@Body() bodySchedule: UpdateManagedUserInput
|
||||||
): Promise<ApiResponse<UserResponse>> {
|
): Promise<UpdateMeOutput> {
|
||||||
const updatedUser = await this.usersRepository.update(user.id, bodySchedule);
|
const updatedUser = await this.usersRepository.update(user.id, bodySchedule);
|
||||||
if (bodySchedule.timeZone && user.defaultScheduleId) {
|
if (bodySchedule.timeZone && user.defaultScheduleId) {
|
||||||
await this.schedulesRepository.updateUserSchedule(user.id, user.defaultScheduleId, {
|
await this.schedulesRepository.updateUserSchedule(user, user.defaultScheduleId, {
|
||||||
timeZone: bodySchedule.timeZone,
|
timeZone: bodySchedule.timeZone,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { MeOutput } from "@/ee/me/outputs/me.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class GetMeOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: MeOutput,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => MeOutput)
|
||||||
|
data!: MeOutput;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { IsInt, IsEmail, IsOptional, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class MeOutput {
|
||||||
|
@IsInt()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
username!: string;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
timeFormat!: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@IsOptional()
|
||||||
|
defaultScheduleId!: number | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
weekStart!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
timeZone!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { MeOutput } from "@/ee/me/outputs/me.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class UpdateMeOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: MeOutput,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => MeOutput)
|
||||||
|
data!: MeOutput;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsEnum } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class ProviderVerifyAccessTokenOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsEnum } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class ProviderVerifyClientOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ProviderVerifyAccessTokenOutput } from "@/ee/provider/outputs/verify-access-token.output";
|
||||||
|
import { ProviderVerifyClientOutput } from "@/ee/provider/outputs/verify-client.output";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
@@ -13,20 +15,21 @@ import {
|
|||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { ApiResponse } from "@calcom/platform-types";
|
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "ee/provider",
|
path: "/provider",
|
||||||
version: "2",
|
version: "2",
|
||||||
})
|
})
|
||||||
|
@DocsTags("Cal provider")
|
||||||
export class CalProviderController {
|
export class CalProviderController {
|
||||||
constructor(private readonly oauthClientRepository: OAuthClientRepository) {}
|
constructor(private readonly oauthClientRepository: OAuthClientRepository) {}
|
||||||
|
|
||||||
@Get("/:clientId")
|
@Get("/:clientId")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async verifyClientId(@Param("clientId") clientId: string): Promise<ApiResponse> {
|
async verifyClientId(@Param("clientId") clientId: string): Promise<ProviderVerifyClientOutput> {
|
||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
@@ -45,7 +48,7 @@ export class CalProviderController {
|
|||||||
async verifyAccessToken(
|
async verifyAccessToken(
|
||||||
@Param("clientId") clientId: string,
|
@Param("clientId") clientId: string,
|
||||||
@GetUser() user: UserWithProfile
|
@GetUser() user: UserWithProfile
|
||||||
): Promise<ApiResponse> {
|
): Promise<ProviderVerifyAccessTokenOutput> {
|
||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
throw new BadRequestException();
|
throw new BadRequestException();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { bootstrap } from "@/app";
|
import { bootstrap } from "@/app";
|
||||||
import { AppModule } from "@/app.module";
|
import { AppModule } from "@/app.module";
|
||||||
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
|
||||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
import { CreateScheduleOutput } from "@/ee/schedules/outputs/create-schedule.output";
|
||||||
|
import { GetSchedulesOutput } from "@/ee/schedules/outputs/get-schedules.output";
|
||||||
|
import { UpdateScheduleOutput } from "@/ee/schedules/outputs/update-schedule.output";
|
||||||
|
import { SchedulesModule } from "@/ee/schedules/schedules.module";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
@@ -17,13 +20,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
|
|||||||
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import {
|
import { UpdateScheduleInput } from "@calcom/platform-types";
|
||||||
ScheduleWithAvailabilities,
|
|
||||||
ScheduleWithAvailabilitiesForWeb,
|
|
||||||
UpdateScheduleOutputType,
|
|
||||||
} from "@calcom/platform-libraries";
|
|
||||||
import { ScheduleResponse, UpdateScheduleInput } from "@calcom/platform-types";
|
|
||||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
|
||||||
|
|
||||||
describe("Schedules Endpoints", () => {
|
describe("Schedules Endpoints", () => {
|
||||||
describe("User Authentication", () => {
|
describe("User Authentication", () => {
|
||||||
@@ -35,14 +32,23 @@ describe("Schedules Endpoints", () => {
|
|||||||
const userEmail = "schedules-controller-e2e@api.com";
|
const userEmail = "schedules-controller-e2e@api.com";
|
||||||
let user: User;
|
let user: User;
|
||||||
|
|
||||||
let createdSchedule: ScheduleResponse;
|
let createdSchedule: CreateScheduleOutput["data"];
|
||||||
|
const defaultAvailabilityDays = [1, 2, 3, 4, 5];
|
||||||
|
const defaultAvailabilityStartTime = "1970-01-01T09:00:00.000Z";
|
||||||
|
const defaultAvailabilityEndTime = "1970-01-01T17:00:00.000Z";
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withAccessTokenAuth(
|
const moduleRef = await withAccessTokenAuth(
|
||||||
userEmail,
|
userEmail,
|
||||||
Test.createTestingModule({
|
Test.createTestingModule({
|
||||||
imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule, TokensModule],
|
imports: [
|
||||||
providers: [SchedulesRepository, SchedulesService],
|
AppModule,
|
||||||
|
PrismaModule,
|
||||||
|
AvailabilitiesModule,
|
||||||
|
UsersModule,
|
||||||
|
TokensModule,
|
||||||
|
SchedulesModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.overrideGuard(PermissionsGuard)
|
.overrideGuard(PermissionsGuard)
|
||||||
@@ -71,10 +77,12 @@ describe("Schedules Endpoints", () => {
|
|||||||
it("should create a default schedule", async () => {
|
it("should create a default schedule", async () => {
|
||||||
const scheduleName = "schedule-name";
|
const scheduleName = "schedule-name";
|
||||||
const scheduleTimeZone = "Europe/Rome";
|
const scheduleTimeZone = "Europe/Rome";
|
||||||
|
const isDefault = true;
|
||||||
|
|
||||||
const body = {
|
const body: CreateScheduleInput = {
|
||||||
name: scheduleName,
|
name: scheduleName,
|
||||||
timeZone: scheduleTimeZone,
|
timeZone: scheduleTimeZone,
|
||||||
|
isDefault,
|
||||||
};
|
};
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
@@ -82,73 +90,25 @@ describe("Schedules Endpoints", () => {
|
|||||||
.send(body)
|
.send(body)
|
||||||
.expect(201)
|
.expect(201)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
const responseBody: ApiSuccessResponse<ScheduleResponse> = response.body;
|
const responseData: CreateScheduleOutput = response.body;
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseData.data).toBeDefined();
|
||||||
expect(responseBody.data.id).toBeDefined();
|
expect(responseData.data.isDefault).toEqual(isDefault);
|
||||||
expect(responseBody.data.userId).toEqual(user.id);
|
expect(responseData.data.timeZone).toEqual(scheduleTimeZone);
|
||||||
expect(responseBody.data.name).toEqual(scheduleName);
|
expect(responseData.data.name).toEqual(scheduleName);
|
||||||
expect(responseBody.data.timeZone).toEqual(scheduleTimeZone);
|
|
||||||
|
|
||||||
expect(responseBody.data.availability).toBeDefined();
|
const schedule = responseData.data.schedule;
|
||||||
expect(responseBody.data.availability?.length).toEqual(1);
|
expect(schedule).toBeDefined();
|
||||||
const defaultAvailabilityDays = [1, 2, 3, 4, 5];
|
expect(schedule.length).toEqual(1);
|
||||||
const defaultAvailabilityStartTime = "09:00:00";
|
expect(schedule?.[0]?.days).toEqual(defaultAvailabilityDays);
|
||||||
const defaultAvailabilityEndTime = "17:00:00";
|
expect(schedule?.[0]?.startTime).toEqual(defaultAvailabilityStartTime);
|
||||||
|
expect(schedule?.[0]?.endTime).toEqual(defaultAvailabilityEndTime);
|
||||||
|
|
||||||
expect(responseBody.data.availability?.[0]?.days).toEqual(defaultAvailabilityDays);
|
const scheduleUser = schedule?.[0].userId
|
||||||
expect(responseBody.data.availability?.[0]?.startTime).toEqual(defaultAvailabilityStartTime);
|
? await userRepositoryFixture.get(schedule?.[0].userId)
|
||||||
expect(responseBody.data.availability?.[0]?.endTime).toEqual(defaultAvailabilityEndTime);
|
: null;
|
||||||
|
expect(scheduleUser?.defaultScheduleId).toEqual(responseData.data.id);
|
||||||
const scheduleUser = await userRepositoryFixture.get(responseBody.data.userId);
|
createdSchedule = responseData.data;
|
||||||
expect(scheduleUser?.defaultScheduleId).toEqual(responseBody.data.id);
|
|
||||||
await scheduleRepositoryFixture.deleteById(responseBody.data.id);
|
|
||||||
await scheduleRepositoryFixture.deleteAvailabilities(responseBody.data.id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create a schedule", async () => {
|
|
||||||
const scheduleName = "schedule-name";
|
|
||||||
const scheduleTimeZone = "Europe/Rome";
|
|
||||||
const availabilityDays = [1, 2, 3, 4, 5, 6];
|
|
||||||
const availabilityStartTime = "11:00:00";
|
|
||||||
const availabilityEndTime = "14:00:00";
|
|
||||||
|
|
||||||
const body = {
|
|
||||||
name: scheduleName,
|
|
||||||
timeZone: scheduleTimeZone,
|
|
||||||
availabilities: [
|
|
||||||
{
|
|
||||||
days: availabilityDays,
|
|
||||||
startTime: availabilityStartTime,
|
|
||||||
endTime: availabilityEndTime,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
|
||||||
.post("/api/v2/schedules")
|
|
||||||
.send(body)
|
|
||||||
.expect(201)
|
|
||||||
.then(async (response) => {
|
|
||||||
const responseBody: ApiSuccessResponse<ScheduleResponse> = response.body;
|
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
|
||||||
expect(responseBody.data).toBeDefined();
|
|
||||||
expect(responseBody.data.id).toBeDefined();
|
|
||||||
expect(responseBody.data.userId).toEqual(user.id);
|
|
||||||
expect(responseBody.data.name).toEqual(scheduleName);
|
|
||||||
expect(responseBody.data.timeZone).toEqual(scheduleTimeZone);
|
|
||||||
|
|
||||||
expect(responseBody.data.availability).toBeDefined();
|
|
||||||
expect(responseBody.data.availability?.length).toEqual(1);
|
|
||||||
expect(responseBody.data.availability?.[0]?.days).toEqual(availabilityDays);
|
|
||||||
expect(responseBody.data.availability?.[0]?.startTime).toEqual(availabilityStartTime);
|
|
||||||
expect(responseBody.data.availability?.[0]?.endTime).toEqual(availabilityEndTime);
|
|
||||||
|
|
||||||
createdSchedule = responseBody.data;
|
|
||||||
|
|
||||||
const scheduleUser = await userRepositoryFixture.get(responseBody.data.userId);
|
|
||||||
expect(scheduleUser?.defaultScheduleId).toEqual(responseBody.data.id);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -156,90 +116,19 @@ describe("Schedules Endpoints", () => {
|
|||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.get("/api/v2/schedules/default")
|
.get("/api/v2/schedules/default")
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then((response) => {
|
.then(async (response) => {
|
||||||
const responseBody: ApiSuccessResponse<ScheduleResponse> = response.body;
|
const responseData: CreateScheduleOutput = response.body;
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseData.data).toBeDefined();
|
||||||
expect(responseBody.data.id).toBeDefined();
|
expect(responseData.data.id).toEqual(createdSchedule.id);
|
||||||
expect(responseBody.data.userId).toEqual(createdSchedule.userId);
|
expect(responseData.data.schedule?.[0].userId).toEqual(createdSchedule.schedule[0].userId);
|
||||||
expect(responseBody.data.name).toEqual(createdSchedule.name);
|
|
||||||
expect(responseBody.data.timeZone).toEqual(createdSchedule.timeZone);
|
|
||||||
|
|
||||||
expect(responseBody.data.availability).toBeDefined();
|
const schedule = responseData.data.schedule;
|
||||||
expect(responseBody.data.availability?.length).toEqual(1);
|
expect(schedule).toBeDefined();
|
||||||
|
expect(schedule.length).toEqual(1);
|
||||||
expect(responseBody.data.availability?.[0]?.days).toEqual(createdSchedule.availability?.[0]?.days);
|
expect(schedule?.[0]?.days).toEqual(defaultAvailabilityDays);
|
||||||
expect(responseBody.data.availability?.[0]?.startTime).toEqual(
|
expect(schedule?.[0]?.startTime).toEqual(defaultAvailabilityStartTime);
|
||||||
createdSchedule.availability?.[0]?.startTime
|
expect(schedule?.[0]?.endTime).toEqual(defaultAvailabilityEndTime);
|
||||||
);
|
|
||||||
expect(responseBody.data.availability?.[0]?.endTime).toEqual(
|
|
||||||
createdSchedule.availability?.[0]?.endTime
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should get schedule", async () => {
|
|
||||||
return request(app.getHttpServer())
|
|
||||||
.get(`/api/v2/schedules/${createdSchedule.id}`)
|
|
||||||
.expect(200)
|
|
||||||
.then((response) => {
|
|
||||||
const responseBody: ApiSuccessResponse<ScheduleResponse> = response.body;
|
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
|
||||||
expect(responseBody.data).toBeDefined();
|
|
||||||
expect(responseBody.data.id).toBeDefined();
|
|
||||||
expect(
|
|
||||||
(responseBody.data as unknown as ScheduleWithAvailabilitiesForWeb).dateOverrides
|
|
||||||
).toBeFalsy();
|
|
||||||
expect(responseBody.data.userId).toEqual(createdSchedule.userId);
|
|
||||||
expect(responseBody.data.name).toEqual(createdSchedule.name);
|
|
||||||
expect(responseBody.data.timeZone).toEqual(createdSchedule.timeZone);
|
|
||||||
|
|
||||||
expect(responseBody.data.availability).toBeDefined();
|
|
||||||
expect(responseBody.data.availability?.length).toEqual(1);
|
|
||||||
|
|
||||||
expect(responseBody.data.availability?.[0]?.days).toEqual(createdSchedule.availability?.[0]?.days);
|
|
||||||
expect(responseBody.data.availability?.[0]?.startTime).toEqual(
|
|
||||||
createdSchedule.availability?.[0]?.startTime
|
|
||||||
);
|
|
||||||
expect(responseBody.data.availability?.[0]?.endTime).toEqual(
|
|
||||||
createdSchedule.availability?.[0]?.endTime
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should get schedule for atom", async () => {
|
|
||||||
const forAtomQueryParam = "?for=atom";
|
|
||||||
return request(app.getHttpServer())
|
|
||||||
.get(`/api/v2/schedules/${createdSchedule.id}${forAtomQueryParam}`)
|
|
||||||
.expect(200)
|
|
||||||
.then((response) => {
|
|
||||||
const responseBody: ApiSuccessResponse<ScheduleWithAvailabilitiesForWeb> = response.body;
|
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
|
||||||
expect(responseBody.data).toBeDefined();
|
|
||||||
expect(responseBody.data.id).toBeDefined();
|
|
||||||
expect(responseBody.data.dateOverrides).toBeDefined();
|
|
||||||
expect((responseBody.data as unknown as ScheduleWithAvailabilities).userId).toBeFalsy();
|
|
||||||
expect(responseBody.data.name).toEqual(createdSchedule.name);
|
|
||||||
expect(responseBody.data.timeZone).toEqual(createdSchedule.timeZone);
|
|
||||||
|
|
||||||
expect(responseBody.data.availability).toBeDefined();
|
|
||||||
expect(responseBody.data.availability?.length).toEqual(7);
|
|
||||||
|
|
||||||
const dateStart = new Date(responseBody.data.availability?.[1]?.[0]?.start);
|
|
||||||
const dateEnd = new Date(responseBody.data.availability?.[1]?.[0]?.end);
|
|
||||||
|
|
||||||
const dateStartHours = dateStart.getUTCHours().toString().padStart(2, "0");
|
|
||||||
const dateStartMinutes = dateStart.getUTCMinutes().toString().padStart(2, "0");
|
|
||||||
const dateStartSeconds = dateStart.getUTCSeconds().toString().padStart(2, "0");
|
|
||||||
const dateEndHours = dateEnd.getUTCHours().toString().padStart(2, "0");
|
|
||||||
const dateEndMinutes = dateEnd.getUTCMinutes().toString().padStart(2, "0");
|
|
||||||
const dateEndSeconds = dateEnd.getUTCSeconds().toString().padStart(2, "0");
|
|
||||||
|
|
||||||
const expectedStart = `${dateStartHours}:${dateStartMinutes}:${dateStartSeconds}`;
|
|
||||||
const expectedEnd = `${dateEndHours}:${dateEndMinutes}:${dateEndSeconds}`;
|
|
||||||
|
|
||||||
expect(expectedStart).toEqual(createdSchedule.availability?.[0]?.startTime);
|
|
||||||
expect(expectedEnd).toEqual(createdSchedule.availability?.[0]?.endTime);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -248,77 +137,26 @@ describe("Schedules Endpoints", () => {
|
|||||||
.get(`/api/v2/schedules`)
|
.get(`/api/v2/schedules`)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const responseBody: ApiSuccessResponse<ScheduleResponse[]> = response.body;
|
const responseData: GetSchedulesOutput = response.body;
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseData.data).toBeDefined();
|
||||||
expect(responseBody.data.length).toEqual(1);
|
expect(responseData.data?.[0].id).toEqual(createdSchedule.id);
|
||||||
|
expect(responseData.data?.[0].schedule?.[0].userId).toEqual(createdSchedule.schedule[0].userId);
|
||||||
|
|
||||||
const fetchedSchedule = responseBody.data[0];
|
const schedule = responseData.data?.[0].schedule;
|
||||||
expect(fetchedSchedule).toBeDefined();
|
expect(schedule).toBeDefined();
|
||||||
expect(fetchedSchedule.userId).toEqual(createdSchedule.userId);
|
expect(schedule.length).toEqual(1);
|
||||||
expect(fetchedSchedule.name).toEqual(createdSchedule.name);
|
expect(schedule?.[0]?.days).toEqual(defaultAvailabilityDays);
|
||||||
expect(fetchedSchedule.timeZone).toEqual(createdSchedule.timeZone);
|
expect(schedule?.[0]?.startTime).toEqual(defaultAvailabilityStartTime);
|
||||||
|
expect(schedule?.[0]?.endTime).toEqual(defaultAvailabilityEndTime);
|
||||||
expect(fetchedSchedule.availability).toBeDefined();
|
|
||||||
expect(fetchedSchedule.availability?.length).toEqual(1);
|
|
||||||
|
|
||||||
expect(fetchedSchedule.availability?.[0]?.days).toEqual(createdSchedule.availability?.[0]?.days);
|
|
||||||
expect(fetchedSchedule.availability?.[0]?.startTime).toEqual(
|
|
||||||
createdSchedule.availability?.[0]?.startTime
|
|
||||||
);
|
|
||||||
expect(fetchedSchedule.availability?.[0]?.endTime).toEqual(
|
|
||||||
createdSchedule.availability?.[0]?.endTime
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update schedule name", async () => {
|
it("should update schedule name", async () => {
|
||||||
const newScheduleName = "new-schedule-name";
|
const newScheduleName = "new-schedule-name";
|
||||||
|
|
||||||
const schedule = [
|
|
||||||
[],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
start: new Date("2024-03-05T11:00:00.000Z"),
|
|
||||||
end: new Date("2024-03-05T14:00:00.000Z"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
start: new Date("2024-03-05T11:00:00.000Z"),
|
|
||||||
end: new Date("2024-03-05T14:00:00.000Z"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
start: new Date("2024-03-05T11:00:00.000Z"),
|
|
||||||
end: new Date("2024-03-05T14:00:00.000Z"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
start: new Date("2024-03-05T11:00:00.000Z"),
|
|
||||||
end: new Date("2024-03-05T14:00:00.000Z"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
start: new Date("2024-03-05T11:00:00.000Z"),
|
|
||||||
end: new Date("2024-03-05T14:00:00.000Z"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
start: new Date("2024-03-05T11:00:00.000Z"),
|
|
||||||
end: new Date("2024-03-05T14:00:00.000Z"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
const body: UpdateScheduleInput = {
|
const body: UpdateScheduleInput = {
|
||||||
scheduleId: createdSchedule.id,
|
|
||||||
name: newScheduleName,
|
name: newScheduleName,
|
||||||
schedule,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
@@ -326,29 +164,19 @@ describe("Schedules Endpoints", () => {
|
|||||||
.send(body)
|
.send(body)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then((response: any) => {
|
.then((response: any) => {
|
||||||
const responseBody: ApiSuccessResponse<UpdateScheduleOutputType> = response.body;
|
const responseData: UpdateScheduleOutput = response.body;
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseData.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseData.data).toBeDefined();
|
||||||
expect(responseBody.data.schedule.id).toBeDefined();
|
expect(responseData.data.schedule.name).toEqual(newScheduleName);
|
||||||
expect(responseBody.data.schedule.userId).toEqual(createdSchedule.userId);
|
expect(responseData.data.schedule.id).toEqual(createdSchedule.id);
|
||||||
expect(responseBody.data.schedule.name).toEqual(newScheduleName);
|
expect(responseData.data.schedule.userId).toEqual(createdSchedule.schedule[0].userId);
|
||||||
expect(responseBody.data.timeZone).toEqual(createdSchedule.timeZone);
|
|
||||||
|
|
||||||
expect(responseBody.data.availability).toBeDefined();
|
const availability = responseData.data.schedule.availability;
|
||||||
const availabilities = responseBody.data?.availability?.filter(
|
expect(availability).toBeDefined();
|
||||||
(availability: any[]) => availability.length
|
expect(availability?.length).toEqual(1);
|
||||||
);
|
expect(availability?.[0]?.days).toEqual(defaultAvailabilityDays);
|
||||||
expect(availabilities?.length).toEqual(6);
|
expect(availability?.[0]?.startTime).toEqual(defaultAvailabilityStartTime);
|
||||||
|
expect(availability?.[0]?.endTime).toEqual(defaultAvailabilityEndTime);
|
||||||
expect(responseBody.data.schedule.availability?.[0]?.days).toEqual(
|
|
||||||
createdSchedule.availability?.[0]?.days
|
|
||||||
);
|
|
||||||
expect(responseBody.data.schedule.availability?.[0]?.startTime).toEqual(
|
|
||||||
`1970-01-01T${createdSchedule.availability?.[0]?.startTime}.000Z`
|
|
||||||
);
|
|
||||||
expect(responseBody.data.schedule.availability?.[0]?.endTime).toEqual(
|
|
||||||
`1970-01-01T${createdSchedule.availability?.[0]?.endTime}.000Z`
|
|
||||||
);
|
|
||||||
|
|
||||||
createdSchedule.name = newScheduleName;
|
createdSchedule.name = newScheduleName;
|
||||||
});
|
});
|
||||||
@@ -360,6 +188,10 @@ describe("Schedules Endpoints", () => {
|
|||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await userRepositoryFixture.deleteByEmail(user.email);
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
|
try {
|
||||||
|
await scheduleRepositoryFixture.deleteById(createdSchedule.id);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { ResponseService } from "@/ee/schedules/services/response/response.service";
|
import { CreateScheduleOutput } from "@/ee/schedules/outputs/create-schedule.output";
|
||||||
|
import { DeleteScheduleOutput } from "@/ee/schedules/outputs/delete-schedule.output";
|
||||||
|
import { GetDefaultScheduleOutput } from "@/ee/schedules/outputs/get-default-schedule.output";
|
||||||
|
import { GetScheduleOutput } from "@/ee/schedules/outputs/get-schedule.output";
|
||||||
|
import { GetSchedulesOutput } from "@/ee/schedules/outputs/get-schedules.output";
|
||||||
|
import { UpdateScheduleOutput } from "@/ee/schedules/outputs/update-schedule.output";
|
||||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
||||||
import { ForAtom } from "@/lib/atoms/decorators/for-atom.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 { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||||
@@ -18,13 +22,10 @@ import {
|
|||||||
Patch,
|
Patch,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
|
import { ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import type { ScheduleWithAvailabilitiesForWeb } from "@calcom/platform-libraries";
|
import { UpdateScheduleInput } from "@calcom/platform-types";
|
||||||
import { updateScheduleHandler } from "@calcom/platform-libraries";
|
|
||||||
import type { UpdateScheduleOutputType } from "@calcom/platform-libraries";
|
|
||||||
import { ApiSuccessResponse, ScheduleResponse, UpdateScheduleInput } from "@calcom/platform-types";
|
|
||||||
import { ApiResponse } from "@calcom/platform-types";
|
|
||||||
|
|
||||||
import { CreateScheduleInput } from "../inputs/create-schedule.input";
|
import { CreateScheduleInput } from "../inputs/create-schedule.input";
|
||||||
|
|
||||||
@@ -33,21 +34,18 @@ import { CreateScheduleInput } from "../inputs/create-schedule.input";
|
|||||||
version: "2",
|
version: "2",
|
||||||
})
|
})
|
||||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
||||||
|
@DocsTags("Schedules")
|
||||||
export class SchedulesController {
|
export class SchedulesController {
|
||||||
constructor(
|
constructor(private readonly schedulesService: SchedulesService) {}
|
||||||
private readonly schedulesService: SchedulesService,
|
|
||||||
private readonly schedulesResponseService: ResponseService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@Permissions([SCHEDULE_WRITE])
|
@Permissions([SCHEDULE_WRITE])
|
||||||
async createSchedule(
|
async createSchedule(
|
||||||
@GetUser() user: UserWithProfile,
|
@GetUser() user: UserWithProfile,
|
||||||
@Body() bodySchedule: CreateScheduleInput,
|
@Body() bodySchedule: CreateScheduleInput
|
||||||
@ForAtom() forAtom: boolean
|
): Promise<CreateScheduleOutput> {
|
||||||
): Promise<ApiSuccessResponse<ScheduleResponse | ScheduleWithAvailabilitiesForWeb>> {
|
|
||||||
const schedule = await this.schedulesService.createUserSchedule(user.id, bodySchedule);
|
const schedule = await this.schedulesService.createUserSchedule(user.id, bodySchedule);
|
||||||
const scheduleFormatted = await this.schedulesResponseService.formatSchedule(forAtom, user, schedule);
|
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
@@ -57,13 +55,11 @@ export class SchedulesController {
|
|||||||
|
|
||||||
@Get("/default")
|
@Get("/default")
|
||||||
@Permissions([SCHEDULE_READ])
|
@Permissions([SCHEDULE_READ])
|
||||||
async getDefaultSchedule(
|
@ApiResponse({ status: 200, description: "Returns the default schedule", type: GetDefaultScheduleOutput })
|
||||||
@GetUser() user: UserWithProfile,
|
async getDefaultSchedule(@GetUser() user: UserWithProfile): Promise<GetDefaultScheduleOutput | null> {
|
||||||
@ForAtom() forAtom: boolean
|
|
||||||
): Promise<ApiResponse<ScheduleResponse | ScheduleWithAvailabilitiesForWeb | null>> {
|
|
||||||
const schedule = await this.schedulesService.getUserScheduleDefault(user.id);
|
const schedule = await this.schedulesService.getUserScheduleDefault(user.id);
|
||||||
const scheduleFormatted = schedule
|
const scheduleFormatted = schedule
|
||||||
? await this.schedulesResponseService.formatSchedule(forAtom, user, schedule)
|
? await this.schedulesService.formatScheduleForAtom(user, schedule)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -76,11 +72,10 @@ export class SchedulesController {
|
|||||||
@Permissions([SCHEDULE_READ])
|
@Permissions([SCHEDULE_READ])
|
||||||
async getSchedule(
|
async getSchedule(
|
||||||
@GetUser() user: UserWithProfile,
|
@GetUser() user: UserWithProfile,
|
||||||
@Param("scheduleId") scheduleId: number,
|
@Param("scheduleId") scheduleId: number
|
||||||
@ForAtom() forAtom: boolean
|
): Promise<GetScheduleOutput> {
|
||||||
): Promise<ApiSuccessResponse<ScheduleResponse | ScheduleWithAvailabilitiesForWeb>> {
|
|
||||||
const schedule = await this.schedulesService.getUserSchedule(user.id, scheduleId);
|
const schedule = await this.schedulesService.getUserSchedule(user.id, scheduleId);
|
||||||
const scheduleFormatted = await this.schedulesResponseService.formatSchedule(forAtom, user, schedule);
|
const scheduleFormatted = await this.schedulesService.formatScheduleForAtom(user, schedule);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
@@ -90,12 +85,9 @@ export class SchedulesController {
|
|||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@Permissions([SCHEDULE_READ])
|
@Permissions([SCHEDULE_READ])
|
||||||
async getSchedules(
|
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput> {
|
||||||
@GetUser() user: UserWithProfile,
|
|
||||||
@ForAtom() forAtom: boolean
|
|
||||||
): Promise<ApiSuccessResponse<ScheduleResponse[] | ScheduleWithAvailabilitiesForWeb[]>> {
|
|
||||||
const schedules = await this.schedulesService.getUserSchedules(user.id);
|
const schedules = await this.schedulesService.getUserSchedules(user.id);
|
||||||
const schedulesFormatted = await this.schedulesResponseService.formatSchedules(forAtom, user, schedules);
|
const schedulesFormatted = await this.schedulesService.formatSchedulesForAtom(user, schedules);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
@@ -108,12 +100,14 @@ export class SchedulesController {
|
|||||||
@Permissions([SCHEDULE_WRITE])
|
@Permissions([SCHEDULE_WRITE])
|
||||||
async updateSchedule(
|
async updateSchedule(
|
||||||
@GetUser() user: UserWithProfile,
|
@GetUser() user: UserWithProfile,
|
||||||
@Body() bodySchedule: UpdateScheduleInput
|
@Body() bodySchedule: UpdateScheduleInput,
|
||||||
): Promise<ApiSuccessResponse<UpdateScheduleOutputType>> {
|
@Param("scheduleId") scheduleId: string
|
||||||
const updatedSchedule: UpdateScheduleOutputType = await updateScheduleHandler({
|
): Promise<UpdateScheduleOutput> {
|
||||||
input: bodySchedule,
|
const updatedSchedule = await this.schedulesService.updateUserSchedule(
|
||||||
ctx: { user },
|
user,
|
||||||
});
|
Number(scheduleId),
|
||||||
|
bodySchedule
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
@@ -127,7 +121,7 @@ export class SchedulesController {
|
|||||||
async deleteSchedule(
|
async deleteSchedule(
|
||||||
@GetUser("id") userId: number,
|
@GetUser("id") userId: number,
|
||||||
@Param("scheduleId") scheduleId: number
|
@Param("scheduleId") scheduleId: number
|
||||||
): Promise<ApiResponse> {
|
): Promise<DeleteScheduleOutput> {
|
||||||
await this.schedulesService.deleteUserSchedule(userId, scheduleId);
|
await this.schedulesService.deleteUserSchedule(userId, scheduleId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -16,6 +16,5 @@ export class CreateScheduleInput {
|
|||||||
availabilities?: CreateAvailabilityInput[];
|
availabilities?: CreateAvailabilityInput[];
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
isDefault!: boolean;
|
||||||
isDefault = true;
|
|
||||||
}
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { CreateAvailabilityInput } from "@/modules/availabilities/inputs/create-availability.input";
|
|
||||||
import { Type } from "class-transformer";
|
|
||||||
import { IsBoolean, IsOptional, IsString, IsTimeZone, ValidateNested } from "class-validator";
|
|
||||||
|
|
||||||
export class UpdateScheduleInput {
|
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
|
||||||
name?: string;
|
|
||||||
|
|
||||||
@IsTimeZone()
|
|
||||||
@IsOptional()
|
|
||||||
timeZone?: string;
|
|
||||||
|
|
||||||
@ValidateNested({ each: true })
|
|
||||||
@Type(() => CreateAvailabilityInput)
|
|
||||||
@IsOptional()
|
|
||||||
availabilities?: CreateAvailabilityInput[];
|
|
||||||
|
|
||||||
@IsBoolean()
|
|
||||||
@IsOptional()
|
|
||||||
isDefault?: boolean;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class CreateScheduleOutput {
|
||||||
|
@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()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => ScheduleOutput)
|
||||||
|
data!: ScheduleOutput;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsEnum } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class DeleteScheduleOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class GetDefaultScheduleOutput {
|
||||||
|
@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()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => ScheduleOutput)
|
||||||
|
data!: ScheduleOutput | null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class GetScheduleOutput {
|
||||||
|
@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()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => ScheduleOutput)
|
||||||
|
data!: ScheduleOutput;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsArray, IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class GetSchedulesOutput {
|
||||||
|
@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()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => ScheduleOutput)
|
||||||
|
@IsArray()
|
||||||
|
data!: ScheduleOutput[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsBoolean, IsInt, IsOptional, IsString, ValidateNested, IsArray } from "class-validator";
|
||||||
|
|
||||||
|
class EventTypeModel {
|
||||||
|
@IsInt()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
eventName?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AvailabilityModel {
|
||||||
|
@IsInt()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
userId?: number | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
scheduleId?: number | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
eventTypeId?: number | null;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsInt({ each: true })
|
||||||
|
days!: number[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Date)
|
||||||
|
@IsString() // Assuming date is serialized/deserialized appropriately
|
||||||
|
startTime?: Date;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Date)
|
||||||
|
@IsString() // Assuming date is serialized/deserialized appropriately
|
||||||
|
endTime?: Date;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Date)
|
||||||
|
@IsString() // Assuming date is serialized/deserialized appropriately
|
||||||
|
date?: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScheduleModel {
|
||||||
|
@IsInt()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
userId!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
timeZone?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => EventTypeModel)
|
||||||
|
@IsArray()
|
||||||
|
eventType?: EventTypeModel[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => AvailabilityModel)
|
||||||
|
@IsArray()
|
||||||
|
availability?: AvailabilityModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdatedScheduleOutput {
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => ScheduleModel)
|
||||||
|
schedule!: ScheduleModel;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
isDefault!: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
timeZone?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
prevDefaultId?: number | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
currentDefaultId?: number | null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsDate, IsOptional, IsArray, IsBoolean, IsInt, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
class AvailabilityModel {
|
||||||
|
@IsInt()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
userId?: number | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
eventTypeId?: number | null;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsInt({ each: true })
|
||||||
|
days!: number[];
|
||||||
|
|
||||||
|
@IsDate()
|
||||||
|
@Type(() => Date)
|
||||||
|
startTime!: Date;
|
||||||
|
|
||||||
|
@IsDate()
|
||||||
|
@Type(() => Date)
|
||||||
|
endTime!: Date;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDate()
|
||||||
|
@Type(() => Date)
|
||||||
|
date?: Date | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
scheduleId?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class WorkingHours {
|
||||||
|
@IsArray()
|
||||||
|
@IsInt({ each: true })
|
||||||
|
days!: number[];
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
startTime!: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
endTime!: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
userId?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimeRange {
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
userId?: number | null;
|
||||||
|
|
||||||
|
@IsDate()
|
||||||
|
start!: Date;
|
||||||
|
|
||||||
|
@IsDate()
|
||||||
|
end!: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScheduleOutput {
|
||||||
|
@IsInt()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
isManaged!: boolean;
|
||||||
|
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => WorkingHours)
|
||||||
|
workingHours!: WorkingHours[];
|
||||||
|
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => AvailabilityModel)
|
||||||
|
@IsArray()
|
||||||
|
schedule!: AvailabilityModel[];
|
||||||
|
|
||||||
|
availability!: TimeRange[][];
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
timeZone!: string;
|
||||||
|
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@IsArray()
|
||||||
|
// note(Lauris) it should be
|
||||||
|
// dateOverrides!: { ranges: TimeRange[] }[];
|
||||||
|
// but docs aren't generating correctly it results in array of strings
|
||||||
|
dateOverrides!: unknown[];
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
isDefault!: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
isLastSchedule!: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
readOnly!: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { UpdatedScheduleOutput } from "@/ee/schedules/outputs/schedule-updated.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class UpdateScheduleOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: UpdatedScheduleOutput,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => UpdatedScheduleOutput)
|
||||||
|
data!: UpdatedScheduleOutput;
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { SchedulesController } from "@/ee/schedules/controllers/schedules.controller";
|
import { SchedulesController } from "@/ee/schedules/controllers/schedules.controller";
|
||||||
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
||||||
import { ResponseService } from "@/ee/schedules/services/response/response.service";
|
|
||||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
||||||
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
@@ -10,8 +9,8 @@ import { Module } from "@nestjs/common";
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, AvailabilitiesModule, UsersModule, TokensModule],
|
imports: [PrismaModule, AvailabilitiesModule, UsersModule, TokensModule],
|
||||||
providers: [SchedulesRepository, SchedulesService, ResponseService],
|
providers: [SchedulesRepository, SchedulesService],
|
||||||
controllers: [SchedulesController],
|
controllers: [SchedulesController],
|
||||||
exports: [SchedulesService],
|
exports: [SchedulesService, SchedulesRepository],
|
||||||
})
|
})
|
||||||
export class SchedulesModule {}
|
export class SchedulesModule {}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
|
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
|
||||||
import { UpdateScheduleInput } from "@/ee/schedules/inputs/update-schedule.input";
|
|
||||||
import { CreateAvailabilityInput } from "@/modules/availabilities/inputs/create-availability.input";
|
import { CreateAvailabilityInput } from "@/modules/availabilities/inputs/create-availability.input";
|
||||||
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 { BadRequestException, Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -78,51 +77,6 @@ export class SchedulesRepository {
|
|||||||
return schedules;
|
return schedules;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateScheduleWithAvailabilities(scheduleId: number, schedule: UpdateScheduleInput) {
|
|
||||||
const existingSchedule = await this.dbRead.prisma.schedule.findUnique({
|
|
||||||
where: { id: scheduleId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!existingSchedule) {
|
|
||||||
throw new BadRequestException(`Schedule with ID=${scheduleId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedScheduleData: Prisma.ScheduleUpdateInput = {
|
|
||||||
user: {
|
|
||||||
connect: {
|
|
||||||
id: existingSchedule.userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (schedule.name) updatedScheduleData.name = schedule.name;
|
|
||||||
if (schedule.timeZone) updatedScheduleData.timeZone = schedule.timeZone;
|
|
||||||
|
|
||||||
if (schedule.availabilities && schedule.availabilities.length > 0) {
|
|
||||||
await this.dbWrite.prisma.availability.deleteMany({
|
|
||||||
where: { scheduleId },
|
|
||||||
});
|
|
||||||
|
|
||||||
updatedScheduleData.availability = {
|
|
||||||
createMany: {
|
|
||||||
data: schedule.availabilities.map((availability) => ({
|
|
||||||
...availability,
|
|
||||||
userId: existingSchedule.userId,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedSchedule = await this.dbWrite.prisma.schedule.update({
|
|
||||||
where: { id: scheduleId },
|
|
||||||
data: updatedScheduleData,
|
|
||||||
include: {
|
|
||||||
availability: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return updatedSchedule;
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteScheduleById(scheduleId: number) {
|
async deleteScheduleById(scheduleId: number) {
|
||||||
return this.dbWrite.prisma.schedule.delete({
|
return this.dbWrite.prisma.schedule.delete({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
|
||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import { User } from "@prisma/client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
transformWorkingHoursForClient,
|
|
||||||
transformAvailabilityForClient,
|
|
||||||
transformDateOverridesForClient,
|
|
||||||
} from "@calcom/platform-libraries";
|
|
||||||
import type {
|
|
||||||
ScheduleWithAvailabilities,
|
|
||||||
ScheduleWithAvailabilitiesForWeb,
|
|
||||||
} from "@calcom/platform-libraries";
|
|
||||||
import { ScheduleResponse, schemaScheduleResponse } from "@calcom/platform-types";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ResponseService {
|
|
||||||
constructor(private readonly schedulesRepository: SchedulesRepository) {}
|
|
||||||
|
|
||||||
async formatSchedule(
|
|
||||||
forAtom: boolean,
|
|
||||||
user: User,
|
|
||||||
schedule: ScheduleWithAvailabilities
|
|
||||||
): Promise<ScheduleResponse | ScheduleWithAvailabilitiesForWeb> {
|
|
||||||
if (forAtom) {
|
|
||||||
const usersSchedulesCount = await this.schedulesRepository.getUserSchedulesCount(user.id);
|
|
||||||
return this.transformScheduleForAtom(schedule, usersSchedulesCount, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
return schemaScheduleResponse.parse(schedule);
|
|
||||||
}
|
|
||||||
|
|
||||||
async formatSchedules(
|
|
||||||
forAtom: boolean,
|
|
||||||
user: User,
|
|
||||||
schedules: ScheduleWithAvailabilities[]
|
|
||||||
): Promise<ScheduleResponse[] | ScheduleWithAvailabilitiesForWeb[]> {
|
|
||||||
if (forAtom) {
|
|
||||||
const usersSchedulesCount = await this.schedulesRepository.getUserSchedulesCount(user.id);
|
|
||||||
return Promise.all(
|
|
||||||
schedules.map((schedule) => this.transformScheduleForAtom(schedule, usersSchedulesCount, user))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return schedules.map((schedule) => schemaScheduleResponse.parse(schedule));
|
|
||||||
}
|
|
||||||
|
|
||||||
async transformScheduleForAtom(
|
|
||||||
schedule: ScheduleWithAvailabilities,
|
|
||||||
userSchedulesCount: number,
|
|
||||||
user: Pick<User, "id" | "defaultScheduleId" | "timeZone">
|
|
||||||
): Promise<ScheduleWithAvailabilitiesForWeb> {
|
|
||||||
const timeZone = schedule.timeZone || user.timeZone;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: schedule.id,
|
|
||||||
name: schedule.name,
|
|
||||||
isManaged: schedule.userId !== user.id,
|
|
||||||
workingHours: transformWorkingHoursForClient(schedule),
|
|
||||||
schedule: schedule.availability,
|
|
||||||
availability: transformAvailabilityForClient(schedule),
|
|
||||||
timeZone,
|
|
||||||
dateOverrides: transformDateOverridesForClient(schedule, timeZone),
|
|
||||||
isDefault: user.defaultScheduleId === schedule.id,
|
|
||||||
isLastSchedule: userSchedulesCount <= 1,
|
|
||||||
readOnly: schedule.userId !== user.id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,20 @@
|
|||||||
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
|
import { CreateScheduleInput } from "@/ee/schedules/inputs/create-schedule.input";
|
||||||
import { UpdateScheduleInput } from "@/ee/schedules/inputs/update-schedule.input";
|
import { ScheduleOutput } from "@/ee/schedules/outputs/schedule.output";
|
||||||
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
||||||
import { AvailabilitiesService } from "@/modules/availabilities/availabilities.service";
|
import { AvailabilitiesService } from "@/modules/availabilities/availabilities.service";
|
||||||
import { UsersRepository } from "@/modules/users/users.repository";
|
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { Schedule } from "@prisma/client";
|
import { Schedule } from "@prisma/client";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
|
||||||
|
import type { ScheduleWithAvailabilities } from "@calcom/platform-libraries";
|
||||||
|
import { updateScheduleHandler } from "@calcom/platform-libraries";
|
||||||
|
import {
|
||||||
|
transformWorkingHoursForClient,
|
||||||
|
transformAvailabilityForClient,
|
||||||
|
transformDateOverridesForClient,
|
||||||
|
} from "@calcom/platform-libraries";
|
||||||
|
import { UpdateScheduleInput } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SchedulesService {
|
export class SchedulesService {
|
||||||
@@ -56,25 +66,31 @@ export class SchedulesService {
|
|||||||
return this.schedulesRepository.getSchedulesByUserId(userId);
|
return this.schedulesRepository.getSchedulesByUserId(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUserSchedule(userId: number, scheduleId: number, schedule: UpdateScheduleInput) {
|
async updateUserSchedule(user: UserWithProfile, scheduleId: number, bodySchedule: UpdateScheduleInput) {
|
||||||
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
|
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
|
||||||
|
|
||||||
if (!existingSchedule) {
|
if (!existingSchedule) {
|
||||||
throw new NotFoundException(`Schedule with ID=${scheduleId} does not exist.`);
|
throw new NotFoundException(`Schedule with ID=${scheduleId} does not exist.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.checkUserOwnsSchedule(userId, existingSchedule);
|
this.checkUserOwnsSchedule(user.id, existingSchedule);
|
||||||
|
|
||||||
const updatedSchedule = await this.schedulesRepository.updateScheduleWithAvailabilities(
|
const schedule = await this.getUserSchedule(user.id, Number(scheduleId));
|
||||||
scheduleId,
|
const scheduleFormatted = await this.formatScheduleForAtom(user, schedule);
|
||||||
schedule
|
|
||||||
);
|
|
||||||
|
|
||||||
if (schedule.isDefault) {
|
if (!bodySchedule.schedule) {
|
||||||
await this.usersRepository.setDefaultSchedule(userId, updatedSchedule.id);
|
// note(Lauris): When updating an availability in cal web app, lets say only its name, also
|
||||||
|
// the schedule is sent and then passed to the update handler. Notably, availability is passed too
|
||||||
|
// and they have same shape, so to match shapes I attach "scheduleFormatted.availability" to reflect
|
||||||
|
// schedule that would be passed by the web app. If we don't, then updating schedule name will erase
|
||||||
|
// schedule.
|
||||||
|
bodySchedule.schedule = scheduleFormatted.availability;
|
||||||
}
|
}
|
||||||
|
|
||||||
return updatedSchedule;
|
return updateScheduleHandler({
|
||||||
|
input: { scheduleId: Number(scheduleId), ...bodySchedule },
|
||||||
|
ctx: { user },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteUserSchedule(userId: number, scheduleId: number) {
|
async deleteUserSchedule(userId: number, scheduleId: number) {
|
||||||
@@ -89,6 +105,44 @@ export class SchedulesService {
|
|||||||
return this.schedulesRepository.deleteScheduleById(scheduleId);
|
return this.schedulesRepository.deleteScheduleById(scheduleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async formatScheduleForAtom(user: User, schedule: ScheduleWithAvailabilities): Promise<ScheduleOutput> {
|
||||||
|
const usersSchedulesCount = await this.schedulesRepository.getUserSchedulesCount(user.id);
|
||||||
|
return this.transformScheduleForAtom(schedule, usersSchedulesCount, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async formatSchedulesForAtom(
|
||||||
|
user: User,
|
||||||
|
schedules: ScheduleWithAvailabilities[]
|
||||||
|
): Promise<ScheduleOutput[]> {
|
||||||
|
const usersSchedulesCount = await this.schedulesRepository.getUserSchedulesCount(user.id);
|
||||||
|
return Promise.all(
|
||||||
|
schedules.map((schedule) => this.transformScheduleForAtom(schedule, usersSchedulesCount, user))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async transformScheduleForAtom(
|
||||||
|
schedule: ScheduleWithAvailabilities,
|
||||||
|
userSchedulesCount: number,
|
||||||
|
user: Pick<User, "id" | "defaultScheduleId" | "timeZone">
|
||||||
|
): Promise<ScheduleOutput> {
|
||||||
|
const timeZone = schedule.timeZone || user.timeZone;
|
||||||
|
const defaultSchedule = await this.getUserScheduleDefault(user.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: schedule.id,
|
||||||
|
name: schedule.name,
|
||||||
|
isManaged: schedule.userId !== user.id,
|
||||||
|
workingHours: transformWorkingHoursForClient(schedule),
|
||||||
|
schedule: schedule.availability,
|
||||||
|
availability: transformAvailabilityForClient(schedule),
|
||||||
|
timeZone,
|
||||||
|
dateOverrides: transformDateOverridesForClient(schedule, timeZone),
|
||||||
|
isDefault: defaultSchedule?.id === schedule.id,
|
||||||
|
isLastSchedule: userSchedulesCount <= 1,
|
||||||
|
readOnly: schedule.userId !== user.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
checkUserOwnsSchedule(userId: number, schedule: Pick<Schedule, "id" | "userId">) {
|
checkUserOwnsSchedule(userId: number, schedule: Pick<Schedule, "id" | "userId">) {
|
||||||
if (userId !== schedule.userId) {
|
if (userId !== schedule.userId) {
|
||||||
throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);
|
throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ async function generateSwagger(app: NestExpressApplication<Server>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(outputFile, JSON.stringify(document, null, 2), { encoding: "utf8" });
|
fs.writeFileSync(outputFile, JSON.stringify(document, null, 2), { encoding: "utf8" });
|
||||||
SwaggerModule.setup("docs", app, document);
|
SwaggerModule.setup("docs", app, document, {
|
||||||
|
customCss: ".swagger-ui .topbar { display: none }",
|
||||||
|
});
|
||||||
|
|
||||||
logger.log(`Swagger documentation available in the "/docs" endpoint\n`);
|
logger.log(`Swagger documentation available in the "/docs" endpoint\n`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { BadRequestException } from "@nestjs/common";
|
import { BadRequestException } from "@nestjs/common";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Transform, TransformFnParams } from "class-transformer";
|
import { Transform, TransformFnParams } from "class-transformer";
|
||||||
import { IsArray, IsDate, IsNumber } from "class-validator";
|
import { IsArray, IsDate, IsNumber } from "class-validator";
|
||||||
|
|
||||||
export class CreateAvailabilityInput {
|
export class CreateAvailabilityInput {
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@IsNumber({}, { each: true })
|
@IsNumber({}, { each: true })
|
||||||
|
@ApiProperty({ example: [1, 2] })
|
||||||
days!: number[];
|
days!: number[];
|
||||||
|
|
||||||
@IsDate()
|
@IsDate()
|
||||||
@@ -17,12 +19,17 @@ export class CreateAvailabilityInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function transformStringToDate(value: string, key: string): Date {
|
function transformStringToDate(value: string, key: string): Date {
|
||||||
const parts = value.split(":");
|
// note(Lauris): incoming value is ISO8061 e.g. 2025-0412T13:17:56.324Z
|
||||||
|
const dateTimeParts = value.split("T");
|
||||||
|
|
||||||
|
const timePart = dateTimeParts[1].split(".")[0]; // Removes milliseconds
|
||||||
|
const parts = timePart.split(":");
|
||||||
|
|
||||||
if (parts.length !== 3) {
|
if (parts.length !== 3) {
|
||||||
throw new BadRequestException(`Invalid ${key} format. Expected format: HH:MM:SS. Received ${value}`);
|
throw new BadRequestException(
|
||||||
|
`Invalid time format. Expected format(ISO8061): 2025-0412T13:17:56.324Z. Received: ${value}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [hours, minutes, seconds] = parts.map(Number);
|
const [hours, minutes, seconds] = parts.map(Number);
|
||||||
|
|
||||||
if (hours < 0 || hours > 23) {
|
if (hours < 0 || hours > 23) {
|
||||||
|
|||||||
@@ -9,16 +9,29 @@ import { APPS_TYPE_ID_MAPPING } from "@calcom/platform-constants";
|
|||||||
export class CredentialsRepository {
|
export class CredentialsRepository {
|
||||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
createAppCredential(type: keyof typeof APPS_TYPE_ID_MAPPING, key: Prisma.InputJsonValue, userId: number) {
|
async createAppCredential(
|
||||||
return this.dbWrite.prisma.credential.create({
|
type: keyof typeof APPS_TYPE_ID_MAPPING,
|
||||||
data: {
|
key: Prisma.InputJsonValue,
|
||||||
|
userId: number
|
||||||
|
) {
|
||||||
|
const credential = await this.getByTypeAndUserId(type, userId);
|
||||||
|
return this.dbWrite.prisma.credential.upsert({
|
||||||
|
create: {
|
||||||
type,
|
type,
|
||||||
key,
|
key,
|
||||||
userId,
|
userId,
|
||||||
appId: APPS_TYPE_ID_MAPPING[type],
|
appId: APPS_TYPE_ID_MAPPING[type],
|
||||||
},
|
},
|
||||||
|
update: {
|
||||||
|
key,
|
||||||
|
invalid: false,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: credential?.id ?? 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getByTypeAndUserId(type: string, userId: number) {
|
getByTypeAndUserId(type: string, userId: number) {
|
||||||
return this.dbWrite.prisma.credential.findFirst({ where: { type, userId } });
|
return this.dbWrite.prisma.credential.findFirst({ where: { type, userId } });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { PlatformEndpointsModule } from "@/ee/platform-endpoints-module";
|
import { PlatformEndpointsModule } from "@/ee/platform-endpoints-module";
|
||||||
import { EventsModule } from "@/modules/events/events.module";
|
|
||||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||||
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
import { TimezoneModule } from "@/modules/timezones/timezones.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";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [EventsModule, OAuthClientModule, PlatformEndpointsModule, TimezoneModule],
|
imports: [OAuthClientModule, PlatformEndpointsModule, TimezoneModule],
|
||||||
})
|
})
|
||||||
export class EndpointsModule implements NestModule {
|
export class EndpointsModule implements NestModule {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
|
||||||
import { Controller, Get, NotFoundException, InternalServerErrorException, Query } from "@nestjs/common";
|
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
|
||||||
import { getPublicEvent } from "@calcom/platform-libraries";
|
|
||||||
import type { PublicEventType } from "@calcom/platform-libraries";
|
|
||||||
import { ApiResponse, GetPublicEventInput } from "@calcom/platform-types";
|
|
||||||
import { PrismaClient } from "@calcom/prisma";
|
|
||||||
|
|
||||||
@Controller({
|
|
||||||
path: "events",
|
|
||||||
version: "2",
|
|
||||||
})
|
|
||||||
export class EventsController {
|
|
||||||
constructor(private readonly prismaReadService: PrismaReadService) {}
|
|
||||||
|
|
||||||
@Get("/public")
|
|
||||||
async getPublicEvent(@Query() queryParams: GetPublicEventInput): Promise<ApiResponse<PublicEventType>> {
|
|
||||||
try {
|
|
||||||
const event = await getPublicEvent(
|
|
||||||
queryParams.username,
|
|
||||||
queryParams.eventSlug,
|
|
||||||
queryParams.isTeamEvent,
|
|
||||||
queryParams.org || null,
|
|
||||||
this.prismaReadService.prisma as unknown as PrismaClient,
|
|
||||||
// We should be fine allowing unpublished orgs events to be servable through platform because Platform access is behind license
|
|
||||||
// If there is ever a need to restrict this, we can introduce a new query param `fromRedirectOfNonOrgLink`
|
|
||||||
true
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
data: event,
|
|
||||||
status: SUCCESS_STATUS,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof Error) {
|
|
||||||
throw new NotFoundException(err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new InternalServerErrorException("Could not find public event.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { EventsController } from "@/modules/events/controllers/events.controller";
|
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
|
||||||
import { Module } from "@nestjs/common";
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [PrismaModule],
|
|
||||||
controllers: [EventsController],
|
|
||||||
})
|
|
||||||
export class EventsModule {}
|
|
||||||
+22
-13
@@ -7,8 +7,8 @@ import {
|
|||||||
CreateUserResponse,
|
CreateUserResponse,
|
||||||
UserReturned,
|
UserReturned,
|
||||||
} from "@/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller";
|
} from "@/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller";
|
||||||
import { CreateManagedPlatformUserInput } from "@/modules/users/inputs/create-managed-platform-user.input";
|
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||||
import { UpdateManagedPlatformUserInput } from "@/modules/users/inputs/update-managed-platform-user.input";
|
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
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";
|
||||||
@@ -78,6 +78,8 @@ describe("OAuth Client Users Endpoints", () => {
|
|||||||
|
|
||||||
let postResponseData: CreateUserResponse;
|
let postResponseData: CreateUserResponse;
|
||||||
|
|
||||||
|
const userEmail = "oauth-client-user@gmail.com";
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await Test.createTestingModule({
|
const moduleRef = await Test.createTestingModule({
|
||||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||||
@@ -117,8 +119,8 @@ describe("OAuth Client Users Endpoints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it(`should fail /POST with incorrect timeZone`, async () => {
|
it(`should fail /POST with incorrect timeZone`, async () => {
|
||||||
const requestBody: CreateManagedPlatformUserInput = {
|
const requestBody: CreateManagedUserInput = {
|
||||||
email: "oauth-client-user@gmail.com",
|
email: userEmail,
|
||||||
timeZone: "incorrect-time-zone",
|
timeZone: "incorrect-time-zone",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -130,8 +132,8 @@ describe("OAuth Client Users Endpoints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it(`/POST`, async () => {
|
it(`/POST`, async () => {
|
||||||
const requestBody: CreateManagedPlatformUserInput = {
|
const requestBody: CreateManagedUserInput = {
|
||||||
email: "oauth-client-user@gmail.com",
|
email: userEmail,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
@@ -150,7 +152,7 @@ describe("OAuth Client Users Endpoints", () => {
|
|||||||
|
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseBody.data).toBeDefined();
|
||||||
expect(responseBody.data.user.email).toEqual(requestBody.email);
|
expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email));
|
||||||
expect(responseBody.data.accessToken).toBeDefined();
|
expect(responseBody.data.accessToken).toBeDefined();
|
||||||
expect(responseBody.data.refreshToken).toBeDefined();
|
expect(responseBody.data.refreshToken).toBeDefined();
|
||||||
|
|
||||||
@@ -197,7 +199,7 @@ describe("OAuth Client Users Endpoints", () => {
|
|||||||
it(`/GET/:id`, async () => {
|
it(`/GET/:id`, async () => {
|
||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.get(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
|
.get(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
|
||||||
.set("Authorization", `Bearer ${postResponseData.accessToken}`)
|
.set("x-cal-secret-key", oAuthClient.secret)
|
||||||
.set("Origin", `${CLIENT_REDIRECT_URI}`)
|
.set("Origin", `${CLIENT_REDIRECT_URI}`)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
@@ -205,16 +207,16 @@ describe("OAuth Client Users Endpoints", () => {
|
|||||||
|
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseBody.data).toBeDefined();
|
||||||
expect(responseBody.data.email).toEqual(postResponseData.user.email);
|
expect(responseBody.data.email).toEqual(getOAuthUserEmail(oAuthClient.id, userEmail));
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/PUT/:id`, async () => {
|
it(`/PUT/:id`, async () => {
|
||||||
const userUpdatedEmail = "pineapple-pizza@gmail.com";
|
const userUpdatedEmail = "pineapple-pizza@gmail.com";
|
||||||
const body: UpdateManagedPlatformUserInput = { email: userUpdatedEmail };
|
const body: UpdateManagedUserInput = { email: userUpdatedEmail };
|
||||||
|
|
||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.patch(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
|
.patch(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
|
||||||
.set("Authorization", `Bearer ${postResponseData.accessToken}`)
|
.set("x-cal-secret-key", oAuthClient.secret)
|
||||||
.set("Origin", `${CLIENT_REDIRECT_URI}`)
|
.set("Origin", `${CLIENT_REDIRECT_URI}`)
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
@@ -223,17 +225,24 @@ describe("OAuth Client Users Endpoints", () => {
|
|||||||
|
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseBody.data).toBeDefined();
|
||||||
expect(responseBody.data.email).toEqual(userUpdatedEmail);
|
expect(responseBody.data.email).toEqual(getOAuthUserEmail(oAuthClient.id, userUpdatedEmail));
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`/DELETE/:id`, () => {
|
it(`/DELETE/:id`, () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.delete(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
|
.delete(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
|
||||||
.set("Authorization", `Bearer ${postResponseData.accessToken}`)
|
.set("x-cal-secret-key", oAuthClient.secret)
|
||||||
.set("Origin", `${CLIENT_REDIRECT_URI}`)
|
.set("Origin", `${CLIENT_REDIRECT_URI}`)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getOAuthUserEmail(oAuthClientId: string, userEmail: string) {
|
||||||
|
const [username, emailDomain] = userEmail.split("@");
|
||||||
|
const email = `${username}+${oAuthClientId}@${emailDomain}`;
|
||||||
|
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||||
await teamRepositoryFixture.delete(organization.id);
|
await teamRepositoryFixture.delete(organization.id);
|
||||||
|
|||||||
+77
-80
@@ -1,10 +1,14 @@
|
|||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { CreateManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
|
||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { GetManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-user.output";
|
||||||
|
import { GetManagedUsersOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-users.output";
|
||||||
|
import { ManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/managed-user.output";
|
||||||
|
import { KeysResponseDto } from "@/modules/oauth-clients/controllers/oauth-flow/responses/KeysResponse.dto";
|
||||||
import { OAuthClientCredentialsGuard } from "@/modules/oauth-clients/guards/oauth-client-credentials/oauth-client-credentials.guard";
|
import { OAuthClientCredentialsGuard } from "@/modules/oauth-clients/guards/oauth-client-credentials/oauth-client-credentials.guard";
|
||||||
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 { CreateManagedPlatformUserInput } from "@/modules/users/inputs/create-managed-platform-user.input";
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
import { UpdateManagedPlatformUserInput } from "@/modules/users/inputs/update-managed-platform-user.input";
|
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||||
|
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||||
import { UsersRepository } from "@/modules/users/users.repository";
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
@@ -15,37 +19,40 @@ import {
|
|||||||
Get,
|
Get,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
NotFoundException,
|
|
||||||
Param,
|
Param,
|
||||||
Patch,
|
Patch,
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
Delete,
|
Delete,
|
||||||
Query,
|
Query,
|
||||||
|
NotFoundException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { ApiResponse, Pagination } from "@calcom/platform-types";
|
import { Pagination } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
path: "oauth-clients/:clientId/users",
|
path: "oauth-clients/:clientId/users",
|
||||||
version: "2",
|
version: "2",
|
||||||
})
|
})
|
||||||
|
@UseGuards(OAuthClientCredentialsGuard)
|
||||||
|
@DocsTags("Managed users")
|
||||||
export class OAuthClientUsersController {
|
export class OAuthClientUsersController {
|
||||||
private readonly logger = new Logger("UserController");
|
private readonly logger = new Logger("UserController");
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly userRepository: UsersRepository,
|
private readonly userRepository: UsersRepository,
|
||||||
private readonly oAuthClientUsersService: OAuthClientUsersService,
|
private readonly oAuthClientUsersService: OAuthClientUsersService,
|
||||||
private readonly oauthRepository: OAuthClientRepository
|
private readonly oauthRepository: OAuthClientRepository,
|
||||||
|
private readonly tokensRepository: TokensRepository
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@UseGuards(OAuthClientCredentialsGuard)
|
|
||||||
async getManagedUsers(
|
async getManagedUsers(
|
||||||
@Param("clientId") oAuthClientId: string,
|
@Param("clientId") oAuthClientId: string,
|
||||||
@Query() queryParams: Pagination
|
@Query() queryParams: Pagination
|
||||||
): Promise<ApiResponse<User[]>> {
|
): Promise<GetManagedUsersOutput> {
|
||||||
this.logger.log(`getting managed users with data for OAuth Client with ID ${oAuthClientId}`);
|
this.logger.log(`getting managed users with data for OAuth Client with ID ${oAuthClientId}`);
|
||||||
const { offset, limit } = queryParams;
|
const { offset, limit } = queryParams;
|
||||||
|
|
||||||
@@ -57,24 +64,18 @@ export class OAuthClientUsersController {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: existingUsers,
|
data: existingUsers.map((user) => this.getResponseUser(user)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@UseGuards(OAuthClientCredentialsGuard)
|
|
||||||
async createUser(
|
async createUser(
|
||||||
@Param("clientId") oAuthClientId: string,
|
@Param("clientId") oAuthClientId: string,
|
||||||
@Body() body: CreateManagedPlatformUserInput
|
@Body() body: CreateManagedUserInput
|
||||||
): Promise<ApiResponse<CreateUserResponse>> {
|
): Promise<CreateManagedUserOutput> {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Creating user with data: ${JSON.stringify(body, null, 2)} for OAuth Client with ID ${oAuthClientId}`
|
`Creating user with data: ${JSON.stringify(body, null, 2)} for OAuth Client with ID ${oAuthClientId}`
|
||||||
);
|
);
|
||||||
const existingUser = await this.userRepository.findByEmail(body.email);
|
|
||||||
|
|
||||||
if (existingUser) {
|
|
||||||
throw new BadRequestException("A user with the provided email already exists.");
|
|
||||||
}
|
|
||||||
const client = await this.oauthRepository.getOAuthClient(oAuthClientId);
|
const client = await this.oauthRepository.getOAuthClient(oAuthClientId);
|
||||||
|
|
||||||
const isPlatformManaged = true;
|
const isPlatformManaged = true;
|
||||||
@@ -88,11 +89,7 @@ export class OAuthClientUsersController {
|
|||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: {
|
data: {
|
||||||
user: {
|
user: this.getResponseUser(user),
|
||||||
id: user.id,
|
|
||||||
email: user.email,
|
|
||||||
username: user.username,
|
|
||||||
},
|
|
||||||
accessToken: tokens.accessToken,
|
accessToken: tokens.accessToken,
|
||||||
refreshToken: tokens.refreshToken,
|
refreshToken: tokens.refreshToken,
|
||||||
},
|
},
|
||||||
@@ -101,99 +98,99 @@ export class OAuthClientUsersController {
|
|||||||
|
|
||||||
@Get("/:userId")
|
@Get("/:userId")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@UseGuards(AccessTokenGuard)
|
|
||||||
async getUserById(
|
async getUserById(
|
||||||
// @Param("clientId") is added to generate OpenAPI schema correctly: clientId is in @Controller path, and unless
|
@Param("clientId") clientId: string,
|
||||||
// also added here as @Param, then it does not appear in OpenAPI schema.
|
|
||||||
@Param("clientId") _: string,
|
|
||||||
@GetUser("id") accessTokenUserId: number,
|
|
||||||
@Param("userId") userId: number
|
@Param("userId") userId: number
|
||||||
): Promise<ApiResponse<UserReturned>> {
|
): Promise<GetManagedUserOutput> {
|
||||||
if (accessTokenUserId !== userId) {
|
const user = await this.validateManagedUserOwnership(clientId, userId);
|
||||||
throw new BadRequestException("userId parameter does not match access token");
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await this.userRepository.findById(userId);
|
|
||||||
if (!user) {
|
|
||||||
throw new NotFoundException(`User with ID ${userId} not found.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: {
|
data: this.getResponseUser(user),
|
||||||
id: user.id,
|
|
||||||
email: user.email,
|
|
||||||
username: user.username,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch("/:userId")
|
@Patch("/:userId")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@UseGuards(AccessTokenGuard)
|
|
||||||
async updateUser(
|
async updateUser(
|
||||||
// @Param("clientId") is added to generate OpenAPI schema correctly: clientId is in @Controller path, and unless
|
@Param("clientId") clientId: string,
|
||||||
// also added here as @Param, then it does not appear in OpenAPI schema.
|
|
||||||
@Param("clientId") _: string,
|
|
||||||
@GetUser("id") accessTokenUserId: number,
|
|
||||||
@Param("userId") userId: number,
|
@Param("userId") userId: number,
|
||||||
@Body() body: UpdateManagedPlatformUserInput
|
@Body() body: UpdateManagedUserInput
|
||||||
): Promise<ApiResponse<UserReturned>> {
|
): Promise<GetManagedUserOutput> {
|
||||||
if (accessTokenUserId !== userId) {
|
await this.validateManagedUserOwnership(clientId, userId);
|
||||||
throw new BadRequestException("userId parameter does not match access token");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(`Updating user with ID ${userId}: ${JSON.stringify(body, null, 2)}`);
|
this.logger.log(`Updating user with ID ${userId}: ${JSON.stringify(body, null, 2)}`);
|
||||||
|
|
||||||
const user = await this.userRepository.update(userId, body);
|
const user = await this.oAuthClientUsersService.updateOAuthClientUser(clientId, userId, body);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: {
|
data: this.getResponseUser(user),
|
||||||
id: user.id,
|
|
||||||
email: user.email,
|
|
||||||
username: user.username,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete("/:userId")
|
@Delete("/:userId")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@UseGuards(AccessTokenGuard)
|
|
||||||
async deleteUser(
|
async deleteUser(
|
||||||
// @Param("clientId") is added to generate OpenAPI schema correctly: clientId is in @Controller path, and unless
|
@Param("clientId") clientId: string,
|
||||||
// also added here as @Param, then it does not appear in OpenAPI schema.
|
|
||||||
@Param("clientId") _: string,
|
|
||||||
@GetUser("id") accessTokenUserId: number,
|
|
||||||
@Param("userId") userId: number
|
@Param("userId") userId: number
|
||||||
): Promise<ApiResponse<UserReturned>> {
|
): Promise<GetManagedUserOutput> {
|
||||||
if (accessTokenUserId !== userId) {
|
const user = await this.validateManagedUserOwnership(clientId, userId);
|
||||||
throw new BadRequestException("userId parameter does not match access token");
|
await this.userRepository.delete(userId);
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(`Deleting user with ID: ${userId}`);
|
this.logger.warn(`Deleting user with ID: ${userId}`);
|
||||||
|
|
||||||
const existingUser = await this.userRepository.findById(userId);
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: this.getResponseUser(user),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!existingUser) {
|
@Post("/:userId/force-refresh")
|
||||||
throw new NotFoundException(`User with ID=${userId} does not exist`);
|
@HttpCode(HttpStatus.OK)
|
||||||
}
|
async forceRefresh(
|
||||||
|
@Param("userId") userId: number,
|
||||||
|
@Param("clientId") oAuthClientId: string
|
||||||
|
): Promise<KeysResponseDto> {
|
||||||
|
this.logger.log(`Forcing new access tokens for managed user with ID ${userId}`);
|
||||||
|
|
||||||
if (!existingUser.isPlatformManaged) {
|
const { id } = await this.validateManagedUserOwnership(oAuthClientId, userId);
|
||||||
throw new BadRequestException(`Can't delete non managed user with ID=${userId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await this.userRepository.delete(userId);
|
const { accessToken, refreshToken } = await this.tokensRepository.createOAuthTokens(
|
||||||
|
oAuthClientId,
|
||||||
|
id,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: {
|
data: {
|
||||||
id: user.id,
|
accessToken,
|
||||||
email: user.email,
|
refreshToken,
|
||||||
username: user.username,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async validateManagedUserOwnership(clientId: string, userId: number): Promise<User> {
|
||||||
|
const user = await this.userRepository.findByIdWithinPlatformScope(userId, clientId);
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException(`User with ID ${userId} is not part of this OAuth client.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getResponseUser(user: User): ManagedUserOutput {
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
username: user.username,
|
||||||
|
timeZone: user.timeZone,
|
||||||
|
weekStart: user.weekStart,
|
||||||
|
createdDate: user.createdDate,
|
||||||
|
timeFormat: user.timeFormat,
|
||||||
|
defaultScheduleId: user.defaultScheduleId,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserReturned = Pick<User, "id" | "email" | "username">;
|
export type UserReturned = Pick<User, "id" | "email" | "username">;
|
||||||
|
|||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
import { ManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/managed-user.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsEnum, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
class CreateManagedUserData {
|
||||||
|
@ApiProperty({
|
||||||
|
type: ManagedUserOutput,
|
||||||
|
})
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => ManagedUserOutput)
|
||||||
|
user!: ManagedUserOutput;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
accessToken!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
refreshToken!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateManagedUserOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: CreateManagedUserData,
|
||||||
|
})
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => CreateManagedUserData)
|
||||||
|
data!: CreateManagedUserData;
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import { ManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/managed-user.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsEnum, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class DeleteManagedUserOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@Type(() => ManagedUserOutput)
|
||||||
|
@ValidateNested()
|
||||||
|
data!: ManagedUserOutput;
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import { ManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/managed-user.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsEnum, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class GetManagedUserOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@Type(() => ManagedUserOutput)
|
||||||
|
@ValidateNested()
|
||||||
|
data!: ManagedUserOutput;
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
import { ManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/managed-user.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsArray, IsEnum, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class GetManagedUsersOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => ManagedUserOutput)
|
||||||
|
@IsArray()
|
||||||
|
data!: ManagedUserOutput[];
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Transform } from "class-transformer";
|
||||||
|
|
||||||
|
export class ManagedUserOutput {
|
||||||
|
@ApiProperty({ example: 1 })
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "alice+cluo37fwd0001khkzqqynkpj3@example.com" })
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "alice" })
|
||||||
|
username!: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "America/New_York" })
|
||||||
|
timeZone!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "Sunday" })
|
||||||
|
weekStart!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "2024-04-01T00:00:00.000Z", type: "string" })
|
||||||
|
@Transform(({ value }) => value.toISOString())
|
||||||
|
createdDate!: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 12, nullable: true })
|
||||||
|
timeFormat!: number | null;
|
||||||
|
|
||||||
|
@ApiProperty({ example: null })
|
||||||
|
defaultScheduleId!: number | null;
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import { ManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/managed-user.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsEnum, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class UpdateManagedUserOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@Type(() => ManagedUserOutput)
|
||||||
|
@ValidateNested()
|
||||||
|
data!: ManagedUserOutput;
|
||||||
|
}
|
||||||
+1
-1
@@ -43,7 +43,7 @@ Second, make sure that the logged in user has organizationId set to pass the Org
|
|||||||
})
|
})
|
||||||
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
|
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
|
||||||
@DocsExcludeController(getEnv("NODE_ENV") === "production")
|
@DocsExcludeController(getEnv("NODE_ENV") === "production")
|
||||||
@DocsTags("Development only")
|
@DocsTags("OAuth - development only")
|
||||||
export class OAuthClientsController {
|
export class OAuthClientsController {
|
||||||
private readonly logger = new Logger("OAuthClientController");
|
private readonly logger = new Logger("OAuthClientController");
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ import { SUCCESS_STATUS, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
|||||||
version: "2",
|
version: "2",
|
||||||
})
|
})
|
||||||
@DocsExcludeController(getEnv("NODE_ENV") === "production")
|
@DocsExcludeController(getEnv("NODE_ENV") === "production")
|
||||||
@DocsTags("Development only")
|
@DocsTags("OAuth - development only")
|
||||||
export class OAuthFlowController {
|
export class OAuthFlowController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly oauthClientRepository: OAuthClientRepository,
|
private readonly oauthClientRepository: OAuthClientRepository,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IsArray, IsOptional, IsString } from "class-validator";
|
import { IsArray, IsBoolean, IsOptional, IsString } from "class-validator";
|
||||||
|
|
||||||
export class UpdateOAuthClientInput {
|
export class UpdateOAuthClientInput {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -13,4 +13,20 @@ export class UpdateOAuthClientInput {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
redirectUris?: string[] = [];
|
redirectUris?: string[] = [];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
bookingRedirectUri?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
bookingCancelRedirectUri?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
bookingRescheduleRedirectUri?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
areEmailsEnabled?: boolean;
|
||||||
}
|
}
|
||||||
@@ -10,13 +10,22 @@ import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-
|
|||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
import { Global, Module } from "@nestjs/common";
|
import { Global, Module } from "@nestjs/common";
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, AuthModule, UsersModule, MembershipsModule, EventTypesModule, OrganizationsModule],
|
imports: [
|
||||||
|
PrismaModule,
|
||||||
|
AuthModule,
|
||||||
|
UsersModule,
|
||||||
|
TokensModule,
|
||||||
|
MembershipsModule,
|
||||||
|
EventTypesModule,
|
||||||
|
OrganizationsModule,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
OAuthClientRepository,
|
OAuthClientRepository,
|
||||||
OAuthClientCredentialsGuard,
|
OAuthClientCredentialsGuard,
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
|
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
|
||||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
import { CreateManagedPlatformUserInput } from "@/modules/users/inputs/create-managed-platform-user.input";
|
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||||
|
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||||
import { UsersRepository } from "@/modules/users/users.repository";
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import * as crypto from "crypto";
|
|
||||||
|
|
||||||
import { createNewUsersConnectToOrgIfExists } from "@calcom/platform-libraries";
|
import { createNewUsersConnectToOrgIfExists } from "@calcom/platform-libraries";
|
||||||
|
import { slugify } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OAuthClientUsersService {
|
export class OAuthClientUsersService {
|
||||||
@@ -18,16 +19,20 @@ export class OAuthClientUsersService {
|
|||||||
|
|
||||||
async createOauthClientUser(
|
async createOauthClientUser(
|
||||||
oAuthClientId: string,
|
oAuthClientId: string,
|
||||||
body: CreateManagedPlatformUserInput,
|
body: CreateManagedUserInput,
|
||||||
isPlatformManaged: boolean,
|
isPlatformManaged: boolean,
|
||||||
organizationId?: number
|
organizationId?: number
|
||||||
) {
|
) {
|
||||||
|
const existsWithEmail = await this.managedUserExistsWithEmail(oAuthClientId, body.email);
|
||||||
|
if (existsWithEmail) {
|
||||||
|
throw new BadRequestException("User with the provided e-mail already exists.");
|
||||||
|
}
|
||||||
|
|
||||||
let user: User;
|
let user: User;
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
throw new BadRequestException("You cannot create a managed user outside of an organization");
|
throw new BadRequestException("You cannot create a managed user outside of an organization");
|
||||||
} else {
|
} else {
|
||||||
const [username, emailDomain] = body.email.split("@");
|
const email = this.getOAuthUserEmail(oAuthClientId, body.email);
|
||||||
const email = `${username}+${oAuthClientId}@${emailDomain}`;
|
|
||||||
user = (
|
user = (
|
||||||
await createNewUsersConnectToOrgIfExists({
|
await createNewUsersConnectToOrgIfExists({
|
||||||
usernamesOrEmails: [email],
|
usernamesOrEmails: [email],
|
||||||
@@ -67,23 +72,28 @@ export class OAuthClientUsersService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function generateShortHash(email: string, clientId: string): string {
|
async managedUserExistsWithEmail(oAuthClientId: string, email: string) {
|
||||||
// Get the current timestamp
|
const oAuthEmail = this.getOAuthUserEmail(oAuthClientId, email);
|
||||||
const timestamp = Date.now().toString();
|
const user = await this.userRepository.findByEmail(oAuthEmail);
|
||||||
|
return !!user;
|
||||||
|
}
|
||||||
|
|
||||||
// Concatenate the timestamp and email
|
async updateOAuthClientUser(oAuthClientId: string, userId: number, body: UpdateManagedUserInput) {
|
||||||
const data = timestamp + email + clientId;
|
if (body.email) {
|
||||||
|
const emailWithOAuthId = this.getOAuthUserEmail(oAuthClientId, body.email);
|
||||||
|
body.email = emailWithOAuthId;
|
||||||
|
const newUsername = slugify(emailWithOAuthId);
|
||||||
|
await this.userRepository.updateUsername(userId, newUsername);
|
||||||
|
}
|
||||||
|
|
||||||
// Create a SHA256 hash
|
return this.userRepository.update(userId, body);
|
||||||
const hash = crypto
|
}
|
||||||
.createHash("sha256")
|
|
||||||
.update(data)
|
|
||||||
.digest("base64")
|
|
||||||
.replace("=", "")
|
|
||||||
.replace("/", "")
|
|
||||||
.replace("+", "");
|
|
||||||
|
|
||||||
return hash.toLowerCase();
|
getOAuthUserEmail(oAuthClientId: string, userEmail: string) {
|
||||||
|
const [username, emailDomain] = userEmail.split("@");
|
||||||
|
const email = `${username}+${oAuthClientId}@${emailDomain}`;
|
||||||
|
|
||||||
|
return email;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,13 +7,26 @@ export class SelectedCalendarsRepository {
|
|||||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
createSelectedCalendar(externalId: string, credentialId: number, userId: number, integration: string) {
|
createSelectedCalendar(externalId: string, credentialId: number, userId: number, integration: string) {
|
||||||
return this.dbWrite.prisma.selectedCalendar.create({
|
return this.dbWrite.prisma.selectedCalendar.upsert({
|
||||||
data: {
|
create: {
|
||||||
userId,
|
userId,
|
||||||
externalId,
|
externalId,
|
||||||
credentialId,
|
credentialId,
|
||||||
integration,
|
integration,
|
||||||
},
|
},
|
||||||
|
update: {
|
||||||
|
userId,
|
||||||
|
externalId,
|
||||||
|
credentialId,
|
||||||
|
integration,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
userId_integration_externalId: {
|
||||||
|
userId,
|
||||||
|
integration,
|
||||||
|
externalId,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||||
import { SlotsService } from "@/modules/slots/services/slots.service";
|
import { SlotsService } from "@/modules/slots/services/slots.service";
|
||||||
import { Query, Body, Controller, Get, Delete, Post, Req, Res, UseGuards } from "@nestjs/common";
|
import { Query, Body, Controller, Get, Delete, Post, Req, Res, UseGuards } from "@nestjs/common";
|
||||||
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
|
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
@@ -13,7 +14,7 @@ import { ApiResponse, GetAvailableSlotsInput } from "@calcom/platform-types";
|
|||||||
path: "slots",
|
path: "slots",
|
||||||
version: "2",
|
version: "2",
|
||||||
})
|
})
|
||||||
@UseGuards(AccessTokenGuard)
|
@DocsTags("Slots")
|
||||||
export class SlotsController {
|
export class SlotsController {
|
||||||
constructor(private readonly slotsService: SlotsService) {}
|
constructor(private readonly slotsService: SlotsService) {}
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ export class SlotsController {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
data: availableSlots,
|
data: availableSlots,
|
||||||
status: "success",
|
status: SUCCESS_STATUS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { TimezonesService } from "@/modules/timezones/services/timezones.service";
|
import { TimezonesService } from "@/modules/timezones/services/timezones.service";
|
||||||
import { Controller, Get } from "@nestjs/common";
|
import { Controller, Get } from "@nestjs/common";
|
||||||
|
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import type { CityTimezones } from "@calcom/platform-libraries";
|
import type { CityTimezones } from "@calcom/platform-libraries";
|
||||||
@@ -9,6 +10,7 @@ import { ApiResponse } from "@calcom/platform-types";
|
|||||||
path: "timezones",
|
path: "timezones",
|
||||||
version: "2",
|
version: "2",
|
||||||
})
|
})
|
||||||
|
@DocsTags("Timezones")
|
||||||
export class TimezonesController {
|
export class TimezonesController {
|
||||||
constructor(private readonly timezonesService: TimezonesService) {}
|
constructor(private readonly timezonesService: TimezonesService) {}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,24 @@ export class TokensRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOAuthTokens(clientId: string, ownerId: number) {
|
async createOAuthTokens(clientId: string, ownerId: number, deleteOld?: boolean) {
|
||||||
|
if (deleteOld) {
|
||||||
|
try {
|
||||||
|
await this.dbWrite.prisma.$transaction([
|
||||||
|
this.dbWrite.prisma.accessToken.deleteMany({
|
||||||
|
where: { client: { id: clientId }, userId: ownerId, expiresAt: { lte: new Date() } },
|
||||||
|
}),
|
||||||
|
this.dbWrite.prisma.refreshToken.deleteMany({
|
||||||
|
where: {
|
||||||
|
client: { id: clientId },
|
||||||
|
userId: ownerId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
} catch (err) {
|
||||||
|
// discard.
|
||||||
|
}
|
||||||
|
}
|
||||||
const accessExpiry = DateTime.now().plus({ minute: 60 }).startOf("minute").toJSDate();
|
const accessExpiry = DateTime.now().plus({ minute: 60 }).startOf("minute").toJSDate();
|
||||||
const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate();
|
const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate();
|
||||||
const [accessToken, refreshToken] = await this.dbWrite.prisma.$transaction([
|
const [accessToken, refreshToken] = await this.dbWrite.prisma.$transaction([
|
||||||
|
|||||||
+6
-1
@@ -1,9 +1,11 @@
|
|||||||
import { IsTimeFormat } from "@/modules/users/inputs/validators/is-time-format";
|
import { IsTimeFormat } from "@/modules/users/inputs/validators/is-time-format";
|
||||||
import { IsWeekStart } from "@/modules/users/inputs/validators/is-week-start";
|
import { IsWeekStart } from "@/modules/users/inputs/validators/is-week-start";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { IsNumber, IsOptional, IsTimeZone, IsString, Validate } from "class-validator";
|
import { IsNumber, IsOptional, IsTimeZone, IsString, Validate } from "class-validator";
|
||||||
|
|
||||||
export class CreateManagedPlatformUserInput {
|
export class CreateManagedUserInput {
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@ApiProperty({ example: "alice@example.com" })
|
||||||
email!: string;
|
email!: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -13,14 +15,17 @@ export class CreateManagedPlatformUserInput {
|
|||||||
@IsNumber()
|
@IsNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Validate(IsTimeFormat)
|
@Validate(IsTimeFormat)
|
||||||
|
@ApiProperty({ example: 12 })
|
||||||
timeFormat?: number;
|
timeFormat?: number;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Validate(IsWeekStart)
|
@Validate(IsWeekStart)
|
||||||
|
@ApiProperty({ example: "Sunday" })
|
||||||
weekStart?: string;
|
weekStart?: string;
|
||||||
|
|
||||||
@IsTimeZone()
|
@IsTimeZone()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@ApiProperty({ example: "America/New_York" })
|
||||||
timeZone?: string;
|
timeZone?: string;
|
||||||
}
|
}
|
||||||
+1
-1
@@ -2,7 +2,7 @@ import { IsTimeFormat } from "@/modules/users/inputs/validators/is-time-format";
|
|||||||
import { IsWeekStart } from "@/modules/users/inputs/validators/is-week-start";
|
import { IsWeekStart } from "@/modules/users/inputs/validators/is-week-start";
|
||||||
import { IsNumber, IsOptional, IsString, IsTimeZone, Validate } from "class-validator";
|
import { IsNumber, IsOptional, IsString, IsTimeZone, Validate } from "class-validator";
|
||||||
|
|
||||||
export class UpdateManagedPlatformUserInput {
|
export class UpdateManagedUserInput {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
email?: string;
|
email?: string;
|
||||||
@@ -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 { CreateManagedPlatformUserInput } from "@/modules/users/inputs/create-managed-platform-user.input";
|
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||||
import { UpdateManagedPlatformUserInput } from "@/modules/users/inputs/update-managed-platform-user.input";
|
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import type { Profile, User } from "@prisma/client";
|
import type { Profile, User } from "@prisma/client";
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ export class UsersRepository {
|
|||||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
user: CreateManagedPlatformUserInput,
|
user: CreateManagedUserInput,
|
||||||
username: string,
|
username: string,
|
||||||
oAuthClientId: string,
|
oAuthClientId: string,
|
||||||
isPlatformManaged: boolean
|
isPlatformManaged: boolean
|
||||||
@@ -52,6 +52,20 @@ export class UsersRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findByIdWithinPlatformScope(userId: number, clientId: string) {
|
||||||
|
return this.dbRead.prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
isPlatformManaged: true,
|
||||||
|
platformOAuthClients: {
|
||||||
|
some: {
|
||||||
|
id: clientId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async findByIdWithProfile(userId: number): Promise<UserWithProfile | null> {
|
async findByIdWithProfile(userId: number): Promise<UserWithProfile | null> {
|
||||||
return this.dbRead.prisma.user.findUnique({
|
return this.dbRead.prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@@ -117,7 +131,7 @@ export class UsersRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(userId: number, updateData: UpdateManagedPlatformUserInput) {
|
async update(userId: number, updateData: UpdateManagedUserInput) {
|
||||||
this.formatInput(updateData);
|
this.formatInput(updateData);
|
||||||
|
|
||||||
return this.dbWrite.prisma.user.update({
|
return this.dbWrite.prisma.user.update({
|
||||||
@@ -126,13 +140,22 @@ export class UsersRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateUsername(userId: number, newUsername: string) {
|
||||||
|
return this.dbWrite.prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: {
|
||||||
|
username: newUsername,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async delete(userId: number): Promise<User> {
|
async delete(userId: number): Promise<User> {
|
||||||
return this.dbWrite.prisma.user.delete({
|
return this.dbWrite.prisma.user.delete({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
formatInput(userInput: CreateManagedPlatformUserInput | UpdateManagedPlatformUserInput) {
|
formatInput(userInput: CreateManagedUserInput | UpdateManagedUserInput) {
|
||||||
if (userInput.weekStart) {
|
if (userInput.weekStart) {
|
||||||
userInput.weekStart = capitalize(userInput.weekStart);
|
userInput.weekStart = capitalize(userInput.weekStart);
|
||||||
}
|
}
|
||||||
|
|||||||
+2387
-209
File diff suppressed because it is too large.
Load diff
@@ -1 +0,0 @@
|
|||||||
Hello World
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
## Cal.com Platform Example (https://platform.cal.com)
|
|
||||||
@@ -1,5 +1,14 @@
|
|||||||
# @calcom/web
|
# @calcom/web
|
||||||
|
|
||||||
|
## 3.9.9
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies
|
||||||
|
- @calcom/embed-core@1.4.0
|
||||||
|
- @calcom/embed-react@1.4.0
|
||||||
|
- @calcom/embed-snippet@1.2.0
|
||||||
|
|
||||||
## 3.1.3
|
## 3.1.3
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
Loaded 100 of 616 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user