Compare commits
109
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55df0e3c9d | ||
|
|
730aab3e3d | ||
|
|
63740c02c7 | ||
|
|
c5942bcfcd | ||
|
|
c4547c51a2 | ||
|
|
cf89046ede | ||
|
|
d5d44fb64b | ||
|
|
4bb092c345 | ||
|
|
e1f7e94e36 | ||
|
|
2e9d5f7df1 | ||
|
|
8c943e8817 | ||
|
|
56821553fe | ||
|
|
b512ceb217 | ||
|
|
cfeb18de3f | ||
|
|
b407be933d | ||
|
|
4e1920471d | ||
|
|
6ee752aed8 | ||
|
|
a1922efab5 | ||
|
|
76332a759b | ||
|
|
dfdda8a5b8 | ||
|
|
286ee6b1bd | ||
|
|
ef8b0fa41c | ||
|
|
7fbeeae25b | ||
|
|
5cf0553199 | ||
|
|
4f114ef8d3 | ||
|
|
a9d6fbd14d | ||
|
|
3e1629add9 | ||
|
|
f1ac0642d1 | ||
|
|
4689775a1f | ||
|
|
4011733271 | ||
|
|
f41aa234d7 | ||
|
|
a44a3e5b84 | ||
|
|
98a075d062 | ||
|
|
bdc3cb9d6e | ||
|
|
616e4c4908 | ||
|
|
eaf695824b | ||
|
|
69a76f7fb7 | ||
|
|
d70fa46260 | ||
|
|
efba58a36a | ||
|
|
b3a28dca25 | ||
|
|
3b471f0e25 | ||
|
|
aac5fa3537 | ||
|
|
134f7e591e | ||
|
|
5bb2bc3ba4 | ||
|
|
bb6f5cca93 | ||
|
|
81bd8033a9 | ||
|
|
c2a44e9319 | ||
|
|
39917ebfa6 | ||
|
|
9ee06b410e | ||
|
|
76ced2d3b8 | ||
|
|
eaa1ad6578 | ||
|
|
f9095d298d | ||
|
|
fd87954048 | ||
|
|
be249a94de | ||
|
|
59582cf8f5 | ||
|
|
5c70060992 | ||
|
|
ae7f8f1653 | ||
|
|
4d0458b0e8 | ||
|
|
55e6682832 | ||
|
|
65448c017f | ||
|
|
e9fd23d0d3 | ||
|
|
badc857bcd | ||
|
|
7fe80dd3dd | ||
|
|
5feca13cfb | ||
|
|
f267cd9cd9 | ||
|
|
e8c2d4fa95 | ||
|
|
7e415b4542 | ||
|
|
43f4caaba4 | ||
|
|
0c2ba81269 | ||
|
|
3aca339c95 | ||
|
|
41553f6c01 | ||
|
|
3661860433 | ||
|
|
f0210d06ea | ||
|
|
fcfb8cc9a3 | ||
|
|
88391c7942 | ||
|
|
e9c6af562e | ||
|
|
18bac4a063 | ||
|
|
0332118724 | ||
|
|
34b62f7516 | ||
|
|
da1863069d | ||
|
|
31270ca86b | ||
|
|
7e876e3457 | ||
|
|
cbfd991ab7 | ||
|
|
d7cd185da1 | ||
|
|
416504240c | ||
|
|
cfadd2e892 | ||
|
|
3aab13b4da | ||
|
|
76bba1ca05 | ||
|
|
06fcea4116 | ||
|
|
a7fecaf578 | ||
|
|
639940d1bf | ||
|
|
d7397d4563 | ||
|
|
cf07621de7 | ||
|
|
110615b509 | ||
|
|
3915348d03 | ||
|
|
5230694e2c | ||
|
|
68f2fc9174 | ||
|
|
a63802dea7 | ||
|
|
2be388373f | ||
|
|
a62c74f79e | ||
|
|
1a8b5d145f | ||
|
|
794c99239a | ||
|
|
1fee90322d | ||
|
|
ee70111c45 | ||
|
|
79169de8d8 | ||
|
|
74288b0ecb | ||
|
|
a977fb4970 | ||
|
|
79a09362c9 | ||
|
|
94a50086e6 |
@@ -319,6 +319,55 @@ dates.map((date) => dayjs.utc(date).add(1, "day").format());
|
||||
dates.map((date) => new Date(date.valueOf() + 24 * 60 * 60 * 1000));
|
||||
```
|
||||
|
||||
## Next.js App Directory: Authorisation Checks in Pages
|
||||
|
||||
This can include checking session.user exists or session.org etc.
|
||||
|
||||
### TL;DR:
|
||||
|
||||
Don’t put permission checks in layout.tsx! Always put them directly inside your page.tsx or relevant server components for every restricted route.
|
||||
|
||||
|
||||
### Why Not to Use Layouts for Permission Checks
|
||||
|
||||
* Layouts don’t intercept all requests: If a user navigates directly or refreshes a protected route, layout checks might be skipped, exposing sensitive content.
|
||||
* APIs and server actions bypass layouts: Sensitive operations running on the server can’t be guarded by checks in the layout.
|
||||
* Risk of data leaks: Only page/server-level checks ensure that unauthorized users never get protected data.
|
||||
|
||||
### ✅ How To Secure Routes (The Right Way)
|
||||
|
||||
* Check permissions inside page.tsx or the actual server component.
|
||||
* Perform all session/user/role validation before querying or rendering sensitive content.
|
||||
* Redirect or return nothing to unauthorized users, before running restricted code.
|
||||
|
||||
### 🛠️ Example: Page-Level Permission Check
|
||||
|
||||
|
||||
```tsx
|
||||
// app/admin/page.tsx
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
import { getUserSession } from "@/lib/auth";
|
||||
|
||||
export default async function AdminPage() {
|
||||
const session = await getUserSession();
|
||||
|
||||
if (!session || session.user.role !== "admin") {
|
||||
redirect("/"); // Or show an error
|
||||
}
|
||||
|
||||
// Protected content here
|
||||
return <div>Welcome, Admin!</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### 🧠 Key Reminders
|
||||
|
||||
* Put permission guards in every restricted page.tsx.
|
||||
* Never assume layouts are secure for guarding data.
|
||||
* Validate users before any sensitive queries or rendering.
|
||||
|
||||
|
||||
## Avoid using Dayjs if you don’t need to be strictly tz aware.
|
||||
|
||||
When doing logic like Dayjs.startOf(".."), you can instead use date-fns' `startOfMonth(dateObj)` / `endOfDay(dateObj)`;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["@calcom/platform-libraries"],
|
||||
"ignore": ["@calcom/platform-libraries", "@calcom/api-v2"],
|
||||
"privatePackages": {
|
||||
"version": false,
|
||||
"tag": false
|
||||
|
||||
@@ -203,6 +203,7 @@ NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
|
||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
|
||||
STRIPE_TEAM_MONTHLY_PRICE_ID=
|
||||
NEXT_PUBLIC_STRIPE_CREDITS_PRICE_ID=
|
||||
ORG_MONTHLY_CREDITS=
|
||||
STRIPE_TEAM_PRODUCT_ID=
|
||||
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
|
||||
STRIPE_ORG_MONTHLY_PRICE_ID=
|
||||
|
||||
+87
-7
@@ -1,4 +1,3 @@
|
||||
// This configuration only applies to the package manager root.
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
extends: ["./packages/config/eslint-preset.js"],
|
||||
@@ -7,26 +6,107 @@ module.exports = {
|
||||
"import/no-cycle": ["warn", { maxDepth: Infinity }],
|
||||
},
|
||||
overrides: [
|
||||
// WARN: features must not be imported by app-store or lib
|
||||
{
|
||||
files: ["packages/lib/**/*.{ts,tsx,js,jsx}", "packages/prisma/**/*.{ts,tsx,js,jsx}"],
|
||||
files: ["packages/app-store/**/*.{ts,tsx,js,jsx}", "packages/lib/**/*.{ts,tsx,js,jsx}"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"warn",
|
||||
{
|
||||
paths: ["@calcom/app-store"],
|
||||
patterns: ["@calcom/app-store/*"],
|
||||
patterns: [
|
||||
{
|
||||
group: [
|
||||
// Catch all relative paths into features
|
||||
"**/features",
|
||||
"**/features/*",
|
||||
// Catch all alias imports
|
||||
"@calcom/features",
|
||||
"@calcom/features/*",
|
||||
],
|
||||
message: "Avoid importing @calcom/features from @calcom/app-store or @calcom/lib.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// WARN: lib must not import app-store or features
|
||||
{
|
||||
files: ["packages/app-store/**/*.{ts,tsx,js,jsx}"],
|
||||
files: ["packages/lib/**/*.{ts,tsx,js,jsx}"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"warn",
|
||||
{
|
||||
paths: ["@calcom/trpc"],
|
||||
patterns: ["@calcom/trpc/*"],
|
||||
patterns: [
|
||||
{
|
||||
group: [
|
||||
// Catch all relative paths into app-store
|
||||
"**/app-store",
|
||||
"**/app-store/*",
|
||||
// Catch all relative paths into features
|
||||
"**/features",
|
||||
"**/features/*",
|
||||
// Catch alias imports
|
||||
"@calcom/app-store",
|
||||
"@calcom/app-store/*",
|
||||
"@calcom/features",
|
||||
"@calcom/features/*",
|
||||
],
|
||||
message: "@calcom/lib should not import @calcom/app-store or @calcom/features.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// ERROR: app-store must not import trpc
|
||||
{
|
||||
files: ["packages/app-store/**/*.{ts,tsx,js,jsx}"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: [
|
||||
// Catch all relative paths into trpc
|
||||
"**/trpc",
|
||||
"**/trpc/*",
|
||||
// Catch alias imports
|
||||
"@calcom/trpc",
|
||||
"@calcom/trpc/*",
|
||||
"@trpc",
|
||||
"@trpc/*",
|
||||
],
|
||||
message:
|
||||
"@calcom/app-store must not import trpc. Move UI to apps/web/components/apps or introduce an API boundary.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// ERROR: prisma must not import `features` package
|
||||
{
|
||||
files: ["packages/prisma/**/*.{ts,tsx,js,jsx}"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: [
|
||||
// Catch all relative paths into features
|
||||
"**/features",
|
||||
"**/features/*",
|
||||
// Catch all alias imports
|
||||
"@calcom/features",
|
||||
"@calcom/features/*",
|
||||
],
|
||||
message: "Avoid importing @calcom/features from @calcom/prisma.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -71,9 +71,9 @@ jobs:
|
||||
- name: Run Tests
|
||||
working-directory: apps/api/v2
|
||||
run: |
|
||||
yarn test:e2e
|
||||
yarn workspace @calcom/platform-libraries build && yarn test:e2e
|
||||
EXIT_CODE=$?
|
||||
echo "yarn test:e2e command exit code: $EXIT_CODE"
|
||||
echo "yarn workspace @calcom/platform-libraries build && yarn test:e2e command exit code: $EXIT_CODE"
|
||||
exit $EXIT_CODE
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
|
||||
@@ -13,6 +13,8 @@ env:
|
||||
ATOMS_E2E_API_URL: ${{ secrets.ATOMS_E2E_API_URL }}
|
||||
ATOMS_E2E_ORG_ID: ${{ secrets.ATOMS_E2E_ORG_ID }}
|
||||
ATOMS_E2E_OAUTH_CLIENT_ID_BOOKER_EMBED: ${{ secrets.ATOMS_E2E_OAUTH_CLIENT_ID_BOOKER_EMBED }}
|
||||
ATOMS_E2E_APPLE_ID: ${{ secrets.ATOMS_E2E_APPLE_ID }}
|
||||
ATOMS_E2E_APPLE_CONNECT_APP_SPECIFIC_PASSCODE: ${{ secrets.ATOMS_E2E_APPLE_CONNECT_APP_SPECIFIC_PASSCODE }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
|
||||
|
||||
@@ -21,4 +21,4 @@ jobs:
|
||||
working-directory: apps/api/v2
|
||||
run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
yarn test
|
||||
yarn workspace @calcom/platform-libraries build && yarn test
|
||||
|
||||
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store"
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/prisma"
|
||||
@@ -13,6 +13,16 @@
|
||||
- `yarn test <filename> -- --integrationTestsOnly` - Run integration tests
|
||||
- `yarn e2e <filename> --grep "<testName>"` - Run specific E2E test
|
||||
|
||||
## Tool Preferences
|
||||
|
||||
### Search Tools Priority
|
||||
|
||||
Use tools in this order of preference:
|
||||
|
||||
1. **ast-grep** - For AST-based code searches (if available)
|
||||
2. **rg (ripgrep)** - For fast text searches
|
||||
3. **grep** - As fallback for text searches
|
||||
|
||||
## 📚 Detailed Documentation
|
||||
|
||||
- **[.agents/README.md](.agents/README.md)** - Complete development guide
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import { captureException as SentryCaptureException } from "@sentry/nextjs";
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
import { redactError } from "@calcom/lib/redactError";
|
||||
|
||||
export const captureErrors: NextMiddleware = async (_req, res, next) => {
|
||||
try {
|
||||
// Catch any errors that are thrown in remaining
|
||||
// middleware and the API route handler
|
||||
await next();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
SentryCaptureException(error);
|
||||
const redactedError = redactError(error);
|
||||
if (redactedError instanceof Error) {
|
||||
res.status(400).json({ message: redactedError.message, error: redactedError });
|
||||
return;
|
||||
}
|
||||
res.status(400).json({ message: "Something went wrong", error });
|
||||
res.status(500).json({ message: "Something went wrong" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ describe("rateLimitApiKey middleware", () => {
|
||||
query: {},
|
||||
userId: testUserId,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(res._getStatusCode()).toBe(401);
|
||||
@@ -43,7 +43,7 @@ describe("rateLimitApiKey middleware", () => {
|
||||
query: { apiKey: "test-key" },
|
||||
userId: testUserId,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(checkRateLimitAndThrowError as any).mockResolvedValueOnce({
|
||||
limit: 100,
|
||||
remaining: 99,
|
||||
@@ -51,6 +51,7 @@ describe("rateLimitApiKey middleware", () => {
|
||||
});
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({
|
||||
@@ -74,15 +75,14 @@ describe("rateLimitApiKey middleware", () => {
|
||||
success: true,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(checkRateLimitAndThrowError as any).mockImplementationOnce(
|
||||
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
|
||||
onRateLimiterResponse(rateLimiterResponse);
|
||||
}
|
||||
);
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(res.getHeader("X-RateLimit-Limit")).toBe(rateLimiterResponse.limit);
|
||||
@@ -96,10 +96,11 @@ describe("rateLimitApiKey middleware", () => {
|
||||
query: { apiKey: "test-key" },
|
||||
userId: testUserId,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(checkRateLimitAndThrowError as any).mockRejectedValue(new Error("Rate limit exceeded"));
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(res._getStatusCode()).toBe(429);
|
||||
@@ -121,6 +122,7 @@ describe("rateLimitApiKey middleware", () => {
|
||||
};
|
||||
|
||||
// Mock rate limiter to trigger the onRateLimiterResponse callback
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(checkRateLimitAndThrowError as any).mockImplementationOnce(
|
||||
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
|
||||
onRateLimiterResponse(rateLimiterResponse);
|
||||
@@ -131,6 +133,7 @@ describe("rateLimitApiKey middleware", () => {
|
||||
vi.mocked(handleAutoLock).mockResolvedValueOnce(true);
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(handleAutoLock).toHaveBeenCalledWith({
|
||||
@@ -158,6 +161,7 @@ describe("rateLimitApiKey middleware", () => {
|
||||
};
|
||||
|
||||
// Mock rate limiter to trigger the onRateLimiterResponse callback
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(checkRateLimitAndThrowError as any).mockImplementationOnce(
|
||||
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
|
||||
onRateLimiterResponse(rateLimiterResponse);
|
||||
@@ -168,6 +172,7 @@ describe("rateLimitApiKey middleware", () => {
|
||||
vi.mocked(handleAutoLock).mockRejectedValueOnce(new Error("No user found for this API key."));
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(handleAutoLock).toHaveBeenCalledWith({
|
||||
@@ -195,6 +200,7 @@ describe("rateLimitApiKey middleware", () => {
|
||||
};
|
||||
|
||||
// Mock rate limiter to trigger the onRateLimiterResponse callback
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(checkRateLimitAndThrowError as any).mockImplementationOnce(
|
||||
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
|
||||
onRateLimiterResponse(rateLimiterResponse);
|
||||
@@ -235,6 +241,7 @@ describe("rateLimitApiKey middleware", () => {
|
||||
);
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(res._getStatusCode()).toBe(429);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
export const eventTypeSelect = {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
length: true,
|
||||
hidden: true,
|
||||
position: true,
|
||||
userId: true,
|
||||
teamId: true,
|
||||
scheduleId: true,
|
||||
eventName: true,
|
||||
timeZone: true,
|
||||
periodType: true,
|
||||
periodStartDate: true,
|
||||
periodEndDate: true,
|
||||
periodDays: true,
|
||||
periodCountCalendarDays: true,
|
||||
requiresConfirmation: true,
|
||||
recurringEvent: true,
|
||||
disableGuests: true,
|
||||
hideCalendarNotes: true,
|
||||
minimumBookingNotice: true,
|
||||
beforeEventBuffer: true,
|
||||
afterEventBuffer: true,
|
||||
schedulingType: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
slotInterval: true,
|
||||
parentId: true,
|
||||
successRedirectUrl: true,
|
||||
description: true,
|
||||
locations: true,
|
||||
metadata: true,
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
bookingFields: true,
|
||||
bookingLimits: true,
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
durationLimits: true,
|
||||
customInputs: {
|
||||
select: { id: true, label: true, required: true, options: true, type: true, placeholder: true },
|
||||
},
|
||||
hashedLink: {
|
||||
select: { link: true },
|
||||
},
|
||||
team: {
|
||||
select: { slug: true },
|
||||
},
|
||||
hosts: {
|
||||
select: {
|
||||
userId: true,
|
||||
isFixed: true,
|
||||
scheduleId: true,
|
||||
},
|
||||
},
|
||||
owner: {
|
||||
select: { username: true, id: true },
|
||||
},
|
||||
children: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
} satisfies Prisma.EventTypeSelect;
|
||||
@@ -6,13 +6,12 @@ import {
|
||||
MIN_EVENT_DURATION_MINUTES,
|
||||
} from "@calcom/lib/constants";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { customInputSchema, eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
import { EventTypeSchema } from "@calcom/prisma/zod/modelSchema/EventTypeSchema";
|
||||
import { HostSchema } from "@calcom/prisma/zod/modelSchema/HostSchema";
|
||||
|
||||
import { Frequency } from "~/lib/types";
|
||||
|
||||
import { jsonSchema } from "./shared/jsonSchema";
|
||||
import { schemaQueryUserId } from "./shared/queryUserId";
|
||||
import { timeZone } from "./shared/timeZone";
|
||||
|
||||
@@ -118,67 +117,3 @@ const schemaEventTypeEditParams = z
|
||||
.strict();
|
||||
|
||||
export const schemaEventTypeEditBodyParams = schemaEventTypeBaseBodyParams.merge(schemaEventTypeEditParams);
|
||||
export const schemaEventTypeReadPublic = EventTypeSchema.pick({
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
length: true,
|
||||
hidden: true,
|
||||
position: true,
|
||||
userId: true,
|
||||
teamId: true,
|
||||
scheduleId: true,
|
||||
eventName: true,
|
||||
timeZone: true,
|
||||
periodType: true,
|
||||
periodStartDate: true,
|
||||
periodEndDate: true,
|
||||
periodDays: true,
|
||||
periodCountCalendarDays: true,
|
||||
requiresConfirmation: true,
|
||||
recurringEvent: true,
|
||||
disableGuests: true,
|
||||
hideCalendarNotes: true,
|
||||
minimumBookingNotice: true,
|
||||
beforeEventBuffer: true,
|
||||
afterEventBuffer: true,
|
||||
schedulingType: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
slotInterval: true,
|
||||
parentId: true,
|
||||
successRedirectUrl: true,
|
||||
description: true,
|
||||
locations: true,
|
||||
metadata: true,
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
bookingFields: true,
|
||||
bookingLimits: true,
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
durationLimits: true,
|
||||
}).merge(
|
||||
z.object({
|
||||
children: z.array(childrenSchema).optional().default([]),
|
||||
hosts: z.array(hostSchema).optional().default([]),
|
||||
locations: z
|
||||
.array(
|
||||
z.object({
|
||||
link: z.string().optional(),
|
||||
address: z.string().optional(),
|
||||
hostPhoneNumber: z.string().optional(),
|
||||
type: z.any().optional(),
|
||||
})
|
||||
)
|
||||
.nullable(),
|
||||
metadata: jsonSchema.nullable(),
|
||||
customInputs: customInputSchema.array().optional(),
|
||||
link: z.string().optional(),
|
||||
hashedLink: z
|
||||
.array(z.object({ link: z.string() }))
|
||||
.optional()
|
||||
.default([]),
|
||||
bookingFields: eventTypeBookingFields.optional().nullable(),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -102,8 +102,11 @@ export async function getHandler(req: NextApiRequest) {
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
||||
attendees: true,
|
||||
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
||||
user: true,
|
||||
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
||||
payment: true,
|
||||
eventType: expand.includes("team") ? { include: { team: true } } : false,
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import {
|
||||
getRecordingsOfCalVideoByRoomName,
|
||||
getDownloadLinkOfCalVideoByRecordingId,
|
||||
} from "@calcom/lib/videoClient";
|
||||
} from "@calcom/app-store/videoClient";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { RecordingItemSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
@@ -63,6 +63,7 @@ export async function getHandler(req: NextApiRequest) {
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id },
|
||||
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
||||
include: { references: true },
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import {
|
||||
getTranscriptsAccessLinkFromRecordingId,
|
||||
checkIfRoomNameMatchesInRecording,
|
||||
} from "@calcom/lib/videoClient";
|
||||
} from "@calcom/app-store/videoClient";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
|
||||
@@ -61,6 +61,7 @@ export async function getHandler(req: NextApiRequest) {
|
||||
const checkIfRecordingBelongsToBooking = async (bookingId: number, recordingId: string) => {
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
||||
include: { references: true },
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/app-store/videoClient";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/lib/videoClient";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
|
||||
@@ -44,6 +44,7 @@ export async function getHandler(req: NextApiRequest) {
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id },
|
||||
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
||||
include: { references: true },
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { getCalendarCredentialsWithoutDelegation, getConnectedCalendars } from "@calcom/lib/CalendarManager";
|
||||
import {
|
||||
getCalendarCredentialsWithoutDelegation,
|
||||
getConnectedCalendars,
|
||||
} from "@calcom/features/calendars/lib/CalendarManager";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { getCalendarCredentialsWithoutDelegation, getConnectedCalendars } from "@calcom/lib/CalendarManager";
|
||||
import {
|
||||
getCalendarCredentialsWithoutDelegation,
|
||||
getConnectedCalendars,
|
||||
} from "@calcom/features/calendars/lib/CalendarManager";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
@@ -66,4 +65,4 @@ async function checkPermissions(req: NextApiRequest) {
|
||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
export default defaultResponder(deleteHandler);
|
||||
export default deleteHandler;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { eventTypeSelect } from "~/lib/selects/event-type";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
import { checkPermissions as canAccessTeamEventOrThrow } from "~/pages/api/teams/[teamId]/_auth-middleware";
|
||||
|
||||
@@ -49,15 +48,13 @@ export async function getHandler(req: NextApiRequest) {
|
||||
|
||||
const eventType = await prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
customInputs: true,
|
||||
hashedLink: { select: { link: true } },
|
||||
team: { select: { slug: true } },
|
||||
hosts: { select: { userId: true, isFixed: true } },
|
||||
owner: { select: { username: true, id: true } },
|
||||
children: { select: { id: true, userId: true } },
|
||||
},
|
||||
select: eventTypeSelect,
|
||||
});
|
||||
|
||||
if (!eventType) {
|
||||
throw new HttpError({ statusCode: 404, message: "Event type not found" });
|
||||
}
|
||||
|
||||
await checkPermissions(req, eventType);
|
||||
|
||||
const link = eventType ? getCalLink(eventType) : null;
|
||||
@@ -74,9 +71,12 @@ export async function getHandler(req: NextApiRequest) {
|
||||
eventType.scheduleId = user.defaultScheduleId;
|
||||
}
|
||||
|
||||
// TODO: eventType when not found should be a 404
|
||||
// but API consumers may depend on the {} behaviour.
|
||||
return { event_type: schemaEventTypeReadPublic.parse({ ...eventType, link }) };
|
||||
return {
|
||||
event_type: {
|
||||
...eventType,
|
||||
link,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type BaseEventTypeCheckPermissions = {
|
||||
@@ -100,4 +100,4 @@ async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
|
||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
export default getHandler;
|
||||
|
||||
@@ -2,13 +2,13 @@ import type { NextApiRequest } from "next";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
import { eventTypeSelect } from "~/lib/selects/event-type";
|
||||
import type { schemaEventTypeBaseBodyParams } from "~/lib/validations/event-type";
|
||||
import { schemaEventTypeEditBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { schemaEventTypeEditBodyParams } from "~/lib/validations/event-type";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
import ensureOnlyMembersAsHosts from "~/pages/api/event-types/_utils/ensureOnlyMembersAsHosts";
|
||||
|
||||
@@ -235,8 +235,8 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
};
|
||||
}
|
||||
await checkPermissions(req, parsedBody);
|
||||
const eventType = await prisma.eventType.update({ where: { id }, data });
|
||||
return { event_type: schemaEventTypeReadPublic.parse(eventType) };
|
||||
const eventType = await prisma.eventType.update({ where: { id }, data, select: eventTypeSelect });
|
||||
return { event_type: eventType };
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaEventTypeBaseBodyParams>) {
|
||||
@@ -249,4 +249,4 @@ async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schema
|
||||
await checkTeamEventEditPermission(req, body);
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
||||
export default patchHandler;
|
||||
|
||||
@@ -2,10 +2,10 @@ import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { eventTypeSelect } from "~/lib/selects/event-type";
|
||||
import { schemaQuerySlug } from "~/lib/validations/shared/querySlug";
|
||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||
|
||||
@@ -54,14 +54,7 @@ async function getHandler(req: NextApiRequest) {
|
||||
userId: shouldUseUserId ? { in: userIds } : undefined,
|
||||
slug: slug, // slug will be undefined if not provided in query
|
||||
},
|
||||
include: {
|
||||
customInputs: true,
|
||||
hashedLink: { select: { link: true } },
|
||||
team: { select: { slug: true } },
|
||||
hosts: { select: { userId: true, isFixed: true } },
|
||||
owner: { select: { username: true, id: true } },
|
||||
children: { select: { id: true, userId: true } },
|
||||
},
|
||||
select: eventTypeSelect,
|
||||
});
|
||||
// this really should return [], but backwards compatibility..
|
||||
if (data.length === 0) new HttpError({ statusCode: 404, message: "No event types were found" });
|
||||
@@ -69,7 +62,7 @@ async function getHandler(req: NextApiRequest) {
|
||||
event_types: (await defaultScheduleId<(typeof data)[number]>({ eventTypes: data, prisma, userIds })).map(
|
||||
(eventType) => {
|
||||
const link = getCalLink(eventType);
|
||||
return schemaEventTypeReadPublic.parse({ ...eventType, link });
|
||||
return { ...eventType, link };
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
@@ -2,11 +2,12 @@ import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { eventTypeSelect } from "~/lib/selects/event-type";
|
||||
import { schemaEventTypeCreateBodyParams } from "~/lib/validations/event-type";
|
||||
import { canUserAccessTeamWithRole } from "~/pages/api/teams/[teamId]/_auth-middleware";
|
||||
|
||||
import checkParentEventOwnership from "./_utils/checkParentEventOwnership";
|
||||
@@ -306,10 +307,10 @@ async function postHandler(req: NextApiRequest) {
|
||||
data.hosts = { createMany: { data: hosts } };
|
||||
}
|
||||
|
||||
const eventType = await prisma.eventType.create({ data, include: { hosts: true } });
|
||||
const eventType = await prisma.eventType.create({ data, select: eventTypeSelect });
|
||||
|
||||
return {
|
||||
event_type: schemaEventTypeReadPublic.parse(eventType),
|
||||
event_type: eventType,
|
||||
message: "Event type created successfully",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export async function paymentById(
|
||||
if (safeQuery.success && method === "GET") {
|
||||
const userWithBookings = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
||||
include: { bookings: true },
|
||||
});
|
||||
await prisma.payment
|
||||
|
||||
@@ -75,7 +75,11 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const data = await prisma.schedule.findUniqueOrThrow({ where: { id }, include: { availability: true } });
|
||||
const data = await prisma.schedule.findUniqueOrThrow({
|
||||
where: { id },
|
||||
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
||||
include: { availability: true },
|
||||
});
|
||||
return { schedule: schemaSchedulePublic.parse(data) };
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
|
||||
/** Only OWNERS and ADMINS can edit teams */
|
||||
const _team = await prisma.team.findFirst({
|
||||
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
||||
include: { members: true },
|
||||
where: { id: teamId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
|
||||
});
|
||||
|
||||
@@ -2,10 +2,10 @@ import type { NextApiRequest } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { eventTypeSelect } from "~/lib/selects/event-type";
|
||||
|
||||
const querySchema = z.object({
|
||||
teamId: z.coerce.number(),
|
||||
@@ -57,17 +57,11 @@ async function getHandler(req: NextApiRequest) {
|
||||
members: { some: { userId } },
|
||||
},
|
||||
},
|
||||
include: {
|
||||
customInputs: true,
|
||||
team: { select: { slug: true } },
|
||||
hosts: { select: { userId: true, isFixed: true } },
|
||||
owner: { select: { username: true, id: true } },
|
||||
children: { select: { id: true, userId: true } },
|
||||
},
|
||||
select: eventTypeSelect,
|
||||
};
|
||||
return {
|
||||
event_types: await prisma.eventType.findMany(args),
|
||||
};
|
||||
|
||||
const data = await prisma.eventType.findMany(args);
|
||||
return { event_types: data.map((eventType) => schemaEventTypeReadPublic.parse(eventType)) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { buildBooking } from "@calcom/lib/test/builder";
|
||||
import {
|
||||
getRecordingsOfCalVideoByRoomName,
|
||||
getDownloadLinkOfCalVideoByRecordingId,
|
||||
} from "@calcom/lib/videoClient";
|
||||
} from "@calcom/app-store/videoClient";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
|
||||
@@ -22,7 +22,7 @@ type CustomNextApiResponse = NextApiResponse & Response;
|
||||
const adminUserId = 1;
|
||||
const memberUserId = 10;
|
||||
|
||||
vi.mock("@calcom/lib/videoClient", () => {
|
||||
vi.mock("@calcom/app-store/videoClient", () => {
|
||||
return {
|
||||
getRecordingsOfCalVideoByRoomName: vi.fn(),
|
||||
getDownloadLinkOfCalVideoByRecordingId: vi.fn(),
|
||||
|
||||
@@ -5,11 +5,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, expect, test, vi, afterEach } from "vitest";
|
||||
|
||||
import { buildBooking } from "@calcom/lib/test/builder";
|
||||
import {
|
||||
getTranscriptsAccessLinkFromRecordingId,
|
||||
checkIfRoomNameMatchesInRecording,
|
||||
} from "@calcom/lib/videoClient";
|
||||
} from "@calcom/app-store/videoClient";
|
||||
import { buildBooking } from "@calcom/lib/test/builder";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
|
||||
@@ -19,7 +19,7 @@ import handler from "../../../../../../pages/api/bookings/[id]/transcripts/[reco
|
||||
type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
vi.mock("@calcom/lib/videoClient", () => {
|
||||
vi.mock("@calcom/app-store/videoClient", () => {
|
||||
return {
|
||||
getTranscriptsAccessLinkFromRecordingId: vi.fn(),
|
||||
checkIfRoomNameMatchesInRecording: vi.fn(),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createMocks } from "node-mocks-http";
|
||||
import { describe, expect, test, vi, afterEach } from "vitest";
|
||||
|
||||
import { buildBooking } from "@calcom/lib/test/builder";
|
||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/lib/videoClient";
|
||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/app-store/videoClient";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
|
||||
@@ -16,7 +16,7 @@ import handler from "../../../../../pages/api/bookings/[id]/transcripts/_get";
|
||||
type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
vi.mock("@calcom/lib/videoClient", () => {
|
||||
vi.mock("@calcom/app-store/videoClient", () => {
|
||||
return {
|
||||
getAllTranscriptsAccessLinkFromRoomName: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -86,7 +86,7 @@ vi.mock("@calcom/lib/di/containers/QualifiedHosts", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/EventManager", () => ({
|
||||
vi.mock("@calcom/features/bookings/lib/EventManager", () => ({
|
||||
default: vi.fn().mockImplementation(() => ({
|
||||
reschedule: vi.fn().mockResolvedValue({
|
||||
results: [],
|
||||
|
||||
@@ -60,7 +60,7 @@ describe("DELETE /api/event-types/[id]", () => {
|
||||
|
||||
describe("Error", async () => {
|
||||
test("Fails to remove event type if user is not OWNER/ADMIN of team associated with event type", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "DELETE",
|
||||
body: {},
|
||||
query: {
|
||||
@@ -71,15 +71,18 @@ describe("DELETE /api/event-types/[id]", () => {
|
||||
// Assign userId to the request objects
|
||||
req.userId = memberUser;
|
||||
|
||||
await handler(req, res);
|
||||
expect(res.statusCode).toBe(403); // Check if the deletion was successful
|
||||
await expect(handler(req)).rejects.toThrowError(
|
||||
expect.objectContaining({
|
||||
statusCode: 403,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Success", async () => {
|
||||
test("Removes event type if user is owner of team associated with event type", async () => {
|
||||
// Mocks for DELETE request
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "DELETE",
|
||||
body: {},
|
||||
query: {
|
||||
@@ -90,8 +93,8 @@ describe("DELETE /api/event-types/[id]", () => {
|
||||
// Assign userId to the request objects
|
||||
req.userId = adminUser;
|
||||
|
||||
await handler(req, res);
|
||||
expect(res.statusCode).toBe(200); // Check if the deletion was successful
|
||||
const deletedEventType = await handler(req);
|
||||
expect(deletedEventType).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ 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>({
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
body: {},
|
||||
query: {
|
||||
@@ -32,16 +32,40 @@ describe("GET /api/event-types/[id]", () => {
|
||||
);
|
||||
|
||||
req.userId = 333333;
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(403);
|
||||
await expect(handler(req)).rejects.toThrowError(
|
||||
expect.objectContaining({
|
||||
statusCode: 403,
|
||||
})
|
||||
);
|
||||
});
|
||||
test("Returns 404 if event type not found", async () => {
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
body: {},
|
||||
query: {
|
||||
id: 123456,
|
||||
},
|
||||
});
|
||||
|
||||
req.isSystemWideAdmin = true;
|
||||
|
||||
prismaMock.eventType.findUnique.mockResolvedValue(null);
|
||||
|
||||
req.userId = 333333;
|
||||
|
||||
await expect(handler(req)).rejects.toThrowError(
|
||||
expect.objectContaining({
|
||||
statusCode: 404,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Success", async () => {
|
||||
test("Returns event type if user is admin", async () => {
|
||||
const eventTypeId = 123456;
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
body: {},
|
||||
query: {
|
||||
@@ -57,17 +81,17 @@ describe("GET /api/event-types/[id]", () => {
|
||||
|
||||
req.isSystemWideAdmin = true;
|
||||
req.userId = 333333;
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId);
|
||||
const responseData = await handler(req);
|
||||
|
||||
expect(responseData.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>({
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
body: {},
|
||||
query: {
|
||||
@@ -94,10 +118,10 @@ describe("GET /api/event-types/[id]", () => {
|
||||
|
||||
req.isSystemWideAdmin = false;
|
||||
req.userId = userId;
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId);
|
||||
const responseData = await handler(req);
|
||||
|
||||
expect(responseData.event_type.id).toEqual(eventTypeId);
|
||||
expect(prismaMock.team.findFirst).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: teamId,
|
||||
@@ -116,7 +140,7 @@ describe("GET /api/event-types/[id]", () => {
|
||||
test("Returns event type if user is the event type owner", async () => {
|
||||
const eventTypeId = 123456;
|
||||
const userId = 333333;
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
body: {},
|
||||
query: {
|
||||
@@ -134,10 +158,10 @@ describe("GET /api/event-types/[id]", () => {
|
||||
|
||||
req.isSystemWideAdmin = false;
|
||||
req.userId = userId;
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId);
|
||||
const responseData = await handler(req);
|
||||
|
||||
expect(responseData.event_type.id).toEqual(eventTypeId);
|
||||
expect(prismaMock.team.findFirst).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,8 @@ RUN yarn workspace @calcom/api-v2 run generate-schemas
|
||||
RUN rm -rf apps/api/v2/node_modules
|
||||
RUN yarn install
|
||||
|
||||
RUN yarn workspace @calcom/platform-libraries run build
|
||||
|
||||
RUN yarn workspace @calcom/api-v2 run build
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.354",
|
||||
"@calcom/platform-libraries": "*",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
|
||||
@@ -15,6 +15,7 @@ import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repo
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { ProfilesModule } from "@/modules/profiles/profiles.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
@@ -33,6 +34,7 @@ import { Module } from "@nestjs/common";
|
||||
EventTypesModule_2024_04_15,
|
||||
SchedulesModule_2024_04_15,
|
||||
EventTypesModule_2024_06_14,
|
||||
ProfilesModule,
|
||||
],
|
||||
providers: [
|
||||
TokensRepository,
|
||||
|
||||
@@ -30,6 +30,7 @@ import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.se
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { ProfilesModule } from "@/modules/profiles/profiles.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
@@ -56,6 +57,7 @@ import { Module } from "@nestjs/common";
|
||||
TeamsModule,
|
||||
TeamsEventTypesModule,
|
||||
MembershipsModule,
|
||||
ProfilesModule,
|
||||
],
|
||||
providers: [
|
||||
TokensRepository,
|
||||
|
||||
@@ -11,7 +11,12 @@ import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/se
|
||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
|
||||
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
|
||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||
import {
|
||||
API_KEY_OR_ACCESS_TOKEN_HEADER,
|
||||
OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER,
|
||||
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
|
||||
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
|
||||
} from "@/lib/docs/headers";
|
||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||
import {
|
||||
AuthOptionalUser,
|
||||
@@ -22,8 +27,8 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
@@ -101,6 +106,9 @@ export class BookingsController_2024_08_13 {
|
||||
|
||||
@Post("/")
|
||||
@UseGuards(OptionalApiAuthGuard)
|
||||
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
||||
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Create a booking",
|
||||
description: `
|
||||
@@ -122,6 +130,9 @@ export class BookingsController_2024_08_13 {
|
||||
And 2 ways to book and event type belonging to a team:
|
||||
1. Provide \`eventTypeId\` in the request body.
|
||||
2. Provide \`eventTypeSlug\` and \`teamSlug\` and optionally \`organizationSlug\` if the team with the teamSlug is within an organization.
|
||||
|
||||
If you are creating a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
||||
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
|
||||
`,
|
||||
})
|
||||
@ApiBody({
|
||||
@@ -161,7 +172,10 @@ export class BookingsController_2024_08_13 {
|
||||
}
|
||||
|
||||
@Get("/:bookingUid")
|
||||
@UseGuards(BookingUidGuard)
|
||||
@UseGuards(BookingUidGuard, OptionalApiAuthGuard)
|
||||
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
||||
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Get a booking",
|
||||
description: `\`:bookingUid\` can be
|
||||
@@ -170,10 +184,17 @@ export class BookingsController_2024_08_13 {
|
||||
|
||||
2. uid of one of the recurring booking recurrences
|
||||
|
||||
3. uid of recurring booking which will return an array of all recurring booking recurrences (stored as recurringBookingUid on one of the individual recurrences).`,
|
||||
3. uid of recurring booking which will return an array of all recurring booking recurrences (stored as recurringBookingUid on one of the individual recurrences).
|
||||
|
||||
If you are fetching a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
||||
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
|
||||
`,
|
||||
})
|
||||
async getBooking(@Param("bookingUid") bookingUid: string): Promise<GetBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.getBooking(bookingUid);
|
||||
async getBooking(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@GetOptionalUser() user: AuthOptionalUser
|
||||
): Promise<GetBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.getBooking(bookingUid, user);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -218,7 +239,7 @@ export class BookingsController_2024_08_13 {
|
||||
@ApiOperation({ summary: "Get all bookings" })
|
||||
async getBookings(
|
||||
@Query() queryParams: GetBookingsInput_2024_08_13,
|
||||
@GetUser() user: UserWithProfile
|
||||
@GetUser() user: ApiAuthGuardUser
|
||||
): Promise<GetBookingsOutput_2024_08_13> {
|
||||
const profile = this.usersService.getUserMainProfile(user);
|
||||
|
||||
@@ -236,7 +257,10 @@ export class BookingsController_2024_08_13 {
|
||||
}
|
||||
|
||||
@Post("/:bookingUid/reschedule")
|
||||
@UseGuards(BookingUidGuard)
|
||||
@UseGuards(BookingUidGuard, OptionalApiAuthGuard)
|
||||
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
||||
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Reschedule a booking",
|
||||
description: "Reschedule a booking or seated booking",
|
||||
@@ -248,17 +272,20 @@ export class BookingsController_2024_08_13 {
|
||||
{ $ref: getSchemaPath(RescheduleSeatedBookingInput_2024_08_13) },
|
||||
],
|
||||
},
|
||||
description:
|
||||
"Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2)",
|
||||
description: `Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2).
|
||||
|
||||
If you are rescheduling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
||||
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.`,
|
||||
})
|
||||
@ApiExtraModels(RescheduleBookingInput_2024_08_13, RescheduleSeatedBookingInput_2024_08_13)
|
||||
async rescheduleBooking(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body(new RescheduleBookingInputPipe())
|
||||
body: RescheduleBookingInput,
|
||||
@Req() request: Request
|
||||
@Req() request: Request,
|
||||
@GetOptionalUser() user: AuthOptionalUser
|
||||
): Promise<RescheduleBookingOutput_2024_08_13> {
|
||||
const newBooking = await this.bookingsService.rescheduleBooking(request, bookingUid, body);
|
||||
const newBooking = await this.bookingsService.rescheduleBooking(request, bookingUid, body, user);
|
||||
await this.bookingsService.billRescheduledBooking(newBooking, bookingUid);
|
||||
|
||||
return {
|
||||
@@ -268,7 +295,10 @@ export class BookingsController_2024_08_13 {
|
||||
}
|
||||
|
||||
@Post("/:bookingUid/cancel")
|
||||
@UseGuards(BookingUidGuard)
|
||||
@UseGuards(BookingUidGuard, OptionalApiAuthGuard)
|
||||
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
||||
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({
|
||||
summary: "Cancel a booking",
|
||||
@@ -281,7 +311,11 @@ export class BookingsController_2024_08_13 {
|
||||
|
||||
\nCancelling recurring seated bookings:
|
||||
For recurring seated bookings it is not possible to cancel all of them with 1 call
|
||||
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.`,
|
||||
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.
|
||||
|
||||
If you are cancelling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
||||
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
|
||||
`,
|
||||
})
|
||||
@ApiBody({
|
||||
schema: {
|
||||
@@ -298,9 +332,10 @@ export class BookingsController_2024_08_13 {
|
||||
@Req() request: Request,
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body(new CancelBookingInputPipe())
|
||||
body: CancelBookingInput
|
||||
body: CancelBookingInput,
|
||||
@GetOptionalUser() user: AuthOptionalUser
|
||||
): Promise<CancelBookingOutput_2024_08_13> {
|
||||
const cancelledBooking = await this.bookingsService.cancelBooking(request, bookingUid, body);
|
||||
const cancelledBooking = await this.bookingsService.cancelBooking(request, bookingUid, body, user);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -342,7 +377,7 @@ export class BookingsController_2024_08_13 {
|
||||
})
|
||||
async reassignBooking(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@GetUser() user: UserWithProfile
|
||||
@GetUser() user: ApiAuthGuardUser
|
||||
): Promise<ReassignBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.reassignBooking(bookingUid, user);
|
||||
|
||||
@@ -392,7 +427,7 @@ export class BookingsController_2024_08_13 {
|
||||
})
|
||||
async confirmBooking(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@GetUser() user: UserWithProfile
|
||||
@GetUser() user: ApiAuthGuardUser
|
||||
): Promise<GetBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.confirmBooking(bookingUid, user);
|
||||
|
||||
@@ -414,7 +449,7 @@ export class BookingsController_2024_08_13 {
|
||||
async declineBooking(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body() body: DeclineBookingInput_2024_08_13,
|
||||
@GetUser() user: UserWithProfile
|
||||
@GetUser() user: ApiAuthGuardUser
|
||||
): Promise<GetBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.declineBooking(bookingUid, user, body.reason);
|
||||
|
||||
|
||||
+201
-53
@@ -437,61 +437,209 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should book an event type with attendees disabled", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)).toISOString(),
|
||||
eventTypeId: seatedEventTypeIdAttendeesDisabledId,
|
||||
attendee: {
|
||||
name: nameAttendeeOne,
|
||||
email: emailAttendeeOne,
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
describe("book an event type with attendees disabled but auth provided", () => {
|
||||
let booking: CreateSeatedBookingOutput_2024_08_13;
|
||||
it("should book an event type with attendees disabled and no auth provided", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)).toISOString(),
|
||||
eventTypeId: seatedEventTypeIdAttendeesDisabledId,
|
||||
attendee: {
|
||||
name: nameAttendeeOne,
|
||||
email: emailAttendeeOne,
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
|
||||
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.seatUid).toBeDefined();
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(
|
||||
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
|
||||
expect(data.eventType).toEqual({
|
||||
id: seatedEventTypeIdAttendeesDisabledId,
|
||||
slug: seatedEventSlugAttendeesDisabled,
|
||||
});
|
||||
expect(data.attendees.length).toEqual(0);
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected seated booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
|
||||
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.seatUid).toBeDefined();
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(
|
||||
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
|
||||
expect(data.eventType).toEqual({
|
||||
id: seatedEventTypeIdAttendeesDisabledId,
|
||||
slug: seatedEventSlugAttendeesDisabled,
|
||||
});
|
||||
expect(data.attendees.length).toEqual(0);
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
booking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected seated booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch booking and not have attendees because no auth provided", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${booking.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsGetSeatedBooking(responseBody.data)) {
|
||||
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts.length).toEqual(1);
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(booking.start);
|
||||
expect(data.end).toEqual(booking.end);
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
|
||||
expect(data.eventType).toEqual({
|
||||
id: seatedEventTypeIdAttendeesDisabledId,
|
||||
slug: seatedEventSlugAttendeesDisabled,
|
||||
});
|
||||
expect(data.attendees.length).toEqual(0);
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
await bookingsRepositoryFixture.deleteById(data.id);
|
||||
} else {
|
||||
throw new Error("Invalid response data - expected booking but received array response");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("book an event type with attendees disabled but auth provided", () => {
|
||||
let booking: CreateSeatedBookingOutput_2024_08_13;
|
||||
|
||||
it("should book an event type with attendees disabled and owner auth provided", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)).toISOString(),
|
||||
eventTypeId: seatedEventTypeIdAttendeesDisabledId,
|
||||
attendee: {
|
||||
name: nameAttendeeOne,
|
||||
email: emailAttendeeOne,
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", apiKeyString)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
|
||||
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.seatUid).toBeDefined();
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(
|
||||
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
|
||||
expect(data.eventType).toEqual({
|
||||
id: seatedEventTypeIdAttendeesDisabledId,
|
||||
slug: seatedEventSlugAttendeesDisabled,
|
||||
});
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0].email).toEqual(emailAttendeeOne);
|
||||
expect(data.attendees[0].name).toEqual(nameAttendeeOne);
|
||||
expect(data.attendees[0].language).toEqual("it");
|
||||
expect(data.attendees[0].seatUid).toBeDefined();
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
booking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected seated booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch booking and have attendees", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${booking.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", apiKeyString)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsGetSeatedBooking(responseBody.data)) {
|
||||
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts.length).toEqual(1);
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(booking.start);
|
||||
expect(data.end).toEqual(booking.end);
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
|
||||
expect(data.eventType).toEqual({
|
||||
id: seatedEventTypeIdAttendeesDisabledId,
|
||||
slug: seatedEventSlugAttendeesDisabled,
|
||||
});
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0].email).toEqual(emailAttendeeOne);
|
||||
expect(data.attendees[0].name).toEqual(nameAttendeeOne);
|
||||
expect(data.attendees[0].language).toEqual("it");
|
||||
expect(data.attendees[0].seatUid).toBeDefined();
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
await bookingsRepositoryFixture.deleteById(data.id);
|
||||
} else {
|
||||
throw new Error("Invalid response data - expected booking but received array response");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancel seated booking", () => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { getPagination } from "@/lib/pagination/pagination";
|
||||
import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
|
||||
@@ -120,7 +121,10 @@ export class BookingsService_2024_08_13 {
|
||||
if (!eventType) {
|
||||
this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body);
|
||||
}
|
||||
await this.checkBookingRequiresAuthenticationSetting(eventType, authUser);
|
||||
const userIsEventTypeAdminOrOwner = authUser
|
||||
? await this.userIsEventTypeAdminOrOwner(authUser, eventType)
|
||||
: false;
|
||||
await this.checkBookingRequiresAuthenticationSetting(eventType, authUser, userIsEventTypeAdminOrOwner);
|
||||
|
||||
if (eventType.schedulingType === "MANAGED") {
|
||||
throw new BadRequestException(
|
||||
@@ -140,13 +144,13 @@ export class BookingsService_2024_08_13 {
|
||||
await this.hasRequiredBookingFieldsResponses(body, eventType);
|
||||
|
||||
if (isRecurring && isSeated) {
|
||||
return await this.createRecurringSeatedBooking(request, body, eventType);
|
||||
return await this.createRecurringSeatedBooking(request, body, eventType, userIsEventTypeAdminOrOwner);
|
||||
}
|
||||
if (isRecurring && !isSeated) {
|
||||
return await this.createRecurringBooking(request, body, eventType);
|
||||
}
|
||||
if (isSeated) {
|
||||
return await this.createSeatedBooking(request, body, eventType);
|
||||
return await this.createSeatedBooking(request, body, eventType, userIsEventTypeAdminOrOwner);
|
||||
}
|
||||
|
||||
return await this.createRegularBooking(request, body, eventType);
|
||||
@@ -157,7 +161,8 @@ export class BookingsService_2024_08_13 {
|
||||
|
||||
async checkBookingRequiresAuthenticationSetting(
|
||||
eventType: EventTypeWithOwnerAndTeam,
|
||||
authUser: AuthOptionalUser
|
||||
authUser: AuthOptionalUser,
|
||||
userIsEventTypeAdminOrOwner: boolean
|
||||
) {
|
||||
if (!eventType.bookingRequiresAuthentication) return true;
|
||||
if (!authUser) {
|
||||
@@ -166,15 +171,23 @@ export class BookingsService_2024_08_13 {
|
||||
);
|
||||
}
|
||||
|
||||
if (!userIsEventTypeAdminOrOwner) {
|
||||
throw new ForbiddenException(
|
||||
"checkBookingRequiresAuthentication - user is not authorized to access this event type. User has to be either event type owner, host, team admin or owner or org admin or owner."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async userIsEventTypeAdminOrOwner(authUser: ApiAuthGuardUser, eventType: EventType) {
|
||||
const authUserId = authUser.id;
|
||||
const authUserRole = authUser.role;
|
||||
const eventTypeId = eventType.id;
|
||||
const teamId = eventType.teamId;
|
||||
const bookingUserId = eventType.owner?.id || null;
|
||||
const eventTypeOwnerId = eventType.userId || null;
|
||||
|
||||
if (authUserRole === "ADMIN") return;
|
||||
if (authUserRole === "ADMIN") return true;
|
||||
|
||||
if (bookingUserId === authUserId) return;
|
||||
if (eventTypeOwnerId === authUserId) return true;
|
||||
|
||||
if (eventTypeId) {
|
||||
const [isUserHost, isUserAssigned] = await Promise.all([
|
||||
@@ -182,7 +195,7 @@ export class BookingsService_2024_08_13 {
|
||||
this.eventTypesRepository.isUserAssignedToEventType(authUserId, eventTypeId),
|
||||
]);
|
||||
|
||||
if (isUserHost || isUserAssigned) return;
|
||||
if (isUserHost || isUserAssigned) return true;
|
||||
}
|
||||
|
||||
if (teamId) {
|
||||
@@ -190,19 +203,17 @@ export class BookingsService_2024_08_13 {
|
||||
authUserId,
|
||||
teamId
|
||||
);
|
||||
if (membership) return;
|
||||
if (membership) return true;
|
||||
}
|
||||
|
||||
if (
|
||||
bookingUserId &&
|
||||
(await this.membershipsService.isUserOrgAdminOrOwnerOfAnotherUser(authUserId, bookingUserId))
|
||||
eventTypeOwnerId &&
|
||||
(await this.membershipsService.isUserOrgAdminOrOwnerOfAnotherUser(authUserId, eventTypeOwnerId))
|
||||
) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new ForbiddenException(
|
||||
"checkBookingRequiresAuthentication - user is not authorized to access this event type. User has to be either event type owner, host, team admin or owner or org admin or owner."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
async getBookedEventType(body: CreateBookingInput) {
|
||||
@@ -476,7 +487,8 @@ export class BookingsService_2024_08_13 {
|
||||
async createRecurringSeatedBooking(
|
||||
request: Request,
|
||||
body: CreateRecurringBookingInput_2024_08_13,
|
||||
eventType: EventTypeWithOwnerAndTeam
|
||||
eventType: EventTypeWithOwnerAndTeam,
|
||||
userIsEventTypeAdminOrOwner: boolean
|
||||
) {
|
||||
const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body, eventType);
|
||||
const bookings = await handleNewRecurringBooking({
|
||||
@@ -491,7 +503,8 @@ export class BookingsService_2024_08_13 {
|
||||
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
|
||||
});
|
||||
return this.outputService.getOutputCreateRecurringSeatedBookings(
|
||||
bookings.map((booking) => ({ uid: booking.uid || "", seatUid: booking.seatReferenceUid || "" }))
|
||||
bookings.map((booking) => ({ uid: booking.uid || "", seatUid: booking.seatReferenceUid || "" })),
|
||||
userIsEventTypeAdminOrOwner
|
||||
);
|
||||
}
|
||||
|
||||
@@ -528,7 +541,8 @@ export class BookingsService_2024_08_13 {
|
||||
async createSeatedBooking(
|
||||
request: Request,
|
||||
body: CreateBookingInput_2024_08_13,
|
||||
eventType: EventTypeWithOwnerAndTeam
|
||||
eventType: EventTypeWithOwnerAndTeam,
|
||||
userIsEventTypeAdminOrOwner: boolean
|
||||
) {
|
||||
const bookingRequest = await this.inputService.createBookingRequest(request, body, eventType);
|
||||
try {
|
||||
@@ -554,7 +568,11 @@ export class BookingsService_2024_08_13 {
|
||||
throw new Error(`Booking with uid=${booking.uid} was not found in the database`);
|
||||
}
|
||||
|
||||
return this.outputService.getOutputCreateSeatedBooking(databaseBooking, booking.seatReferenceUid || "");
|
||||
return this.outputService.getOutputCreateSeatedBooking(
|
||||
databaseBooking,
|
||||
booking.seatReferenceUid || "",
|
||||
userIsEventTypeAdminOrOwner
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message === "booking_seats_full_error") {
|
||||
@@ -565,8 +583,12 @@ export class BookingsService_2024_08_13 {
|
||||
}
|
||||
}
|
||||
|
||||
async getBooking(uid: string) {
|
||||
async getBooking(uid: string, authUser: AuthOptionalUser) {
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesWithBookingSeatAndUserAndEvent(uid);
|
||||
const userIsEventTypeAdminOrOwner =
|
||||
authUser && booking?.eventType
|
||||
? await this.userIsEventTypeAdminOrOwner(authUser, booking.eventType)
|
||||
: false;
|
||||
|
||||
if (booking) {
|
||||
const isRecurring = !!booking.recurringEventId;
|
||||
@@ -576,13 +598,12 @@ export class BookingsService_2024_08_13 {
|
||||
return this.outputService.getOutputRecurringBooking(booking);
|
||||
}
|
||||
if (isRecurring && isSeated) {
|
||||
return this.outputService.getOutputRecurringSeatedBooking(
|
||||
booking,
|
||||
!!booking.eventType?.seatsShowAttendees
|
||||
);
|
||||
const showAttendees = userIsEventTypeAdminOrOwner || !!booking.eventType?.seatsShowAttendees;
|
||||
return this.outputService.getOutputRecurringSeatedBooking(booking, showAttendees);
|
||||
}
|
||||
if (isSeated) {
|
||||
return this.outputService.getOutputSeatedBooking(booking, !!booking.eventType?.seatsShowAttendees);
|
||||
const showAttendees = userIsEventTypeAdminOrOwner || !!booking.eventType?.seatsShowAttendees;
|
||||
return this.outputService.getOutputSeatedBooking(booking, showAttendees);
|
||||
}
|
||||
return this.outputService.getOutputBooking(booking);
|
||||
}
|
||||
@@ -594,10 +615,9 @@ export class BookingsService_2024_08_13 {
|
||||
const ids = recurringBooking.map((booking) => booking.id);
|
||||
const isRecurringSeated = !!recurringBooking[0].eventType?.seatsPerTimeSlot;
|
||||
if (isRecurringSeated) {
|
||||
return this.outputService.getOutputRecurringSeatedBookings(
|
||||
ids,
|
||||
!!recurringBooking[0].eventType?.seatsShowAttendees
|
||||
);
|
||||
const showAttendees =
|
||||
userIsEventTypeAdminOrOwner || !!recurringBooking[0].eventType?.seatsShowAttendees;
|
||||
return this.outputService.getOutputRecurringSeatedBookings(ids, showAttendees);
|
||||
}
|
||||
|
||||
return this.outputService.getOutputRecurringBookings(ids);
|
||||
@@ -663,19 +683,9 @@ export class BookingsService_2024_08_13 {
|
||||
if (isRecurring && !isSeated) {
|
||||
formattedBookings.push(this.outputService.getOutputRecurringBooking(formatted));
|
||||
} else if (isRecurring && isSeated) {
|
||||
formattedBookings.push(
|
||||
this.outputService.getOutputRecurringSeatedBooking(
|
||||
formatted,
|
||||
!!formatted.eventType?.seatsShowAttendees
|
||||
)
|
||||
);
|
||||
formattedBookings.push(this.outputService.getOutputRecurringSeatedBooking(formatted, true));
|
||||
} else if (isSeated) {
|
||||
formattedBookings.push(
|
||||
await this.outputService.getOutputSeatedBooking(
|
||||
formatted,
|
||||
!!formatted.eventType?.seatsShowAttendees
|
||||
)
|
||||
);
|
||||
formattedBookings.push(await this.outputService.getOutputSeatedBooking(formatted, true));
|
||||
} else {
|
||||
formattedBookings.push(await this.outputService.getOutputBooking(formatted));
|
||||
}
|
||||
@@ -718,7 +728,12 @@ export class BookingsService_2024_08_13 {
|
||||
return queryParamsAttendeeEmail;
|
||||
}
|
||||
|
||||
async rescheduleBooking(request: Request, bookingUid: string, body: RescheduleBookingInput) {
|
||||
async rescheduleBooking(
|
||||
request: Request,
|
||||
bookingUid: string,
|
||||
body: RescheduleBookingInput,
|
||||
authUser: AuthOptionalUser
|
||||
) {
|
||||
try {
|
||||
await this.canRescheduleBooking(bookingUid);
|
||||
const bookingRequest = await this.inputService.createRescheduleBookingRequest(
|
||||
@@ -747,6 +762,10 @@ export class BookingsService_2024_08_13 {
|
||||
throw new Error(`Booking with uid=${booking.uid} was not found in the database`);
|
||||
}
|
||||
|
||||
const userIsEventTypeAdminOrOwner =
|
||||
authUser && databaseBooking.eventType
|
||||
? await this.userIsEventTypeAdminOrOwner(authUser, databaseBooking.eventType)
|
||||
: false;
|
||||
const isRecurring = !!databaseBooking.recurringEventId;
|
||||
const isSeated = !!databaseBooking.eventType?.seatsPerTimeSlot;
|
||||
|
||||
@@ -756,13 +775,15 @@ export class BookingsService_2024_08_13 {
|
||||
if (isRecurring && isSeated) {
|
||||
return this.outputService.getOutputCreateRecurringSeatedBooking(
|
||||
databaseBooking,
|
||||
booking?.seatReferenceUid || ""
|
||||
booking?.seatReferenceUid || "",
|
||||
userIsEventTypeAdminOrOwner
|
||||
);
|
||||
}
|
||||
if (isSeated) {
|
||||
return this.outputService.getOutputCreateSeatedBooking(
|
||||
databaseBooking,
|
||||
booking.seatReferenceUid || ""
|
||||
booking.seatReferenceUid || "",
|
||||
userIsEventTypeAdminOrOwner
|
||||
);
|
||||
}
|
||||
return this.outputService.getOutputBooking(databaseBooking);
|
||||
@@ -790,7 +811,12 @@ export class BookingsService_2024_08_13 {
|
||||
return booking;
|
||||
}
|
||||
|
||||
async cancelBooking(request: Request, bookingUid: string, body: CancelBookingInput) {
|
||||
async cancelBooking(
|
||||
request: Request,
|
||||
bookingUid: string,
|
||||
body: CancelBookingInput,
|
||||
authUser: AuthOptionalUser
|
||||
) {
|
||||
if (this.inputService.isCancelSeatedBody(body)) {
|
||||
const seat = await this.bookingSeatRepository.getByReferenceUid(body.seatUid);
|
||||
|
||||
@@ -821,13 +847,13 @@ export class BookingsService_2024_08_13 {
|
||||
}
|
||||
|
||||
if ("cancelSubsequentBookings" in body && body.cancelSubsequentBookings) {
|
||||
return this.getAllRecurringBookingsByIndividualUid(bookingUid);
|
||||
return this.getAllRecurringBookingsByIndividualUid(bookingUid, authUser);
|
||||
}
|
||||
|
||||
return this.getBooking(bookingUid);
|
||||
return this.getBooking(bookingUid, authUser);
|
||||
}
|
||||
|
||||
private async getAllRecurringBookingsByIndividualUid(bookingUid: string) {
|
||||
private async getAllRecurringBookingsByIndividualUid(bookingUid: string, authUser: AuthOptionalUser) {
|
||||
const booking = await this.bookingsRepository.getByUid(bookingUid);
|
||||
const recurringBookingUid = booking?.recurringEventId;
|
||||
if (!recurringBookingUid) {
|
||||
@@ -836,7 +862,7 @@ export class BookingsService_2024_08_13 {
|
||||
);
|
||||
}
|
||||
|
||||
return await this.getBooking(recurringBookingUid);
|
||||
return await this.getBooking(recurringBookingUid, authUser);
|
||||
}
|
||||
|
||||
async markAbsent(bookingUid: string, bookingOwnerId: number, body: MarkAbsentBookingInput_2024_08_13) {
|
||||
@@ -1003,7 +1029,7 @@ export class BookingsService_2024_08_13 {
|
||||
}
|
||||
}
|
||||
|
||||
async confirmBooking(bookingUid: string, requestUser: UserWithProfile) {
|
||||
async confirmBooking(bookingUid: string, requestUser: ApiAuthGuardUser) {
|
||||
const booking = await this.bookingsRepository.getByUid(bookingUid);
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
@@ -1032,10 +1058,10 @@ export class BookingsService_2024_08_13 {
|
||||
},
|
||||
});
|
||||
|
||||
return this.getBooking(bookingUid);
|
||||
return this.getBooking(bookingUid, requestUser);
|
||||
}
|
||||
|
||||
async declineBooking(bookingUid: string, requestUser: UserWithProfile, reason?: string) {
|
||||
async declineBooking(bookingUid: string, requestUser: ApiAuthGuardUser, reason?: string) {
|
||||
const booking = await this.bookingsRepository.getByUid(bookingUid);
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
@@ -1065,7 +1091,7 @@ export class BookingsService_2024_08_13 {
|
||||
},
|
||||
});
|
||||
|
||||
return this.getBooking(bookingUid);
|
||||
return this.getBooking(bookingUid, requestUser);
|
||||
}
|
||||
|
||||
async getCalendarLinks(bookingUid: string): Promise<CalendarLink[]> {
|
||||
|
||||
@@ -176,6 +176,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
metadata: inputBooking.metadata || {},
|
||||
hasHashedBookingLink: false,
|
||||
guests,
|
||||
verificationCode: inputBooking.emailVerificationCode,
|
||||
// note(Lauris): responses with name and email are required by the handleNewBooking
|
||||
responses: {
|
||||
...(inputBooking.bookingFieldsResponses || {}),
|
||||
@@ -471,6 +472,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
metadata: inputBooking.metadata || {},
|
||||
hasHashedBookingLink: false,
|
||||
guests,
|
||||
verificationCode: inputBooking.emailVerificationCode,
|
||||
// note(Lauris): responses with name and email are required by the handleNewBooking
|
||||
responses: {
|
||||
...(inputBooking.bookingFieldsResponses || {}),
|
||||
@@ -600,6 +602,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
guests: [],
|
||||
responses: { ...bookingResponses },
|
||||
rescheduleUid: inputBooking.seatUid,
|
||||
verificationCode: inputBooking.emailVerificationCode,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -661,6 +664,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
guests: bookingResponses.guests,
|
||||
responses: { ...bookingResponses, rescheduledReason: inputBooking.reschedulingReason },
|
||||
rescheduleUid: bookingUid,
|
||||
verificationCode: inputBooking.emailVerificationCode,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -229,7 +229,8 @@ export class OutputBookingsService_2024_08_13 {
|
||||
status: databaseBooking.status.toLowerCase(),
|
||||
cancellationReason:
|
||||
databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancellationReason : undefined,
|
||||
cancelledByEmail: databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancelledBy : undefined,
|
||||
cancelledByEmail:
|
||||
databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancelledBy : undefined,
|
||||
reschedulingReason: bookingResponses?.rescheduledReason,
|
||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||
start: databaseBooking.startTime,
|
||||
@@ -269,12 +270,11 @@ export class OutputBookingsService_2024_08_13 {
|
||||
|
||||
async getOutputCreateSeatedBooking(
|
||||
databaseBooking: DatabaseBooking,
|
||||
seatUid: string
|
||||
seatUid: string,
|
||||
userIsEventTypeAdminOrOwner: boolean
|
||||
): Promise<CreateSeatedBookingOutput_2024_08_13> {
|
||||
const getSeatedBookingOutput = await this.getOutputSeatedBooking(
|
||||
databaseBooking,
|
||||
!!databaseBooking.eventType?.seatsShowAttendees
|
||||
);
|
||||
const showAttendees = userIsEventTypeAdminOrOwner || !!databaseBooking.eventType?.seatsShowAttendees;
|
||||
const getSeatedBookingOutput = await this.getOutputSeatedBooking(databaseBooking, showAttendees);
|
||||
return { ...getSeatedBookingOutput, seatUid };
|
||||
}
|
||||
|
||||
@@ -377,7 +377,10 @@ export class OutputBookingsService_2024_08_13 {
|
||||
return transformed.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
||||
}
|
||||
|
||||
async getOutputCreateRecurringSeatedBookings(bookings: { uid: string; seatUid: string }[]) {
|
||||
async getOutputCreateRecurringSeatedBookings(
|
||||
bookings: { uid: string; seatUid: string }[],
|
||||
userIsEventTypeAdminOrOwner: boolean
|
||||
) {
|
||||
const transformed = [];
|
||||
|
||||
for (const booking of bookings) {
|
||||
@@ -386,7 +389,13 @@ export class OutputBookingsService_2024_08_13 {
|
||||
if (!databaseBooking) {
|
||||
throw new Error(`Booking with uid=${booking.uid} was not found in the database`);
|
||||
}
|
||||
transformed.push(this.getOutputCreateRecurringSeatedBooking(databaseBooking, booking.seatUid));
|
||||
transformed.push(
|
||||
this.getOutputCreateRecurringSeatedBooking(
|
||||
databaseBooking,
|
||||
booking.seatUid,
|
||||
userIsEventTypeAdminOrOwner
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return transformed.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
||||
@@ -394,11 +403,13 @@ export class OutputBookingsService_2024_08_13 {
|
||||
|
||||
getOutputCreateRecurringSeatedBooking(
|
||||
databaseBooking: DatabaseBooking,
|
||||
seatUid: string
|
||||
seatUid: string,
|
||||
userIsEventTypeAdminOrOwner: boolean
|
||||
): CreateRecurringSeatedBookingOutput_2024_08_13 {
|
||||
const showAttendees = userIsEventTypeAdminOrOwner || !!databaseBooking.eventType?.seatsShowAttendees;
|
||||
const getRecurringSeatedBookingOutput = this.getOutputRecurringSeatedBooking(
|
||||
databaseBooking,
|
||||
!!databaseBooking.eventType?.seatsShowAttendees
|
||||
showAttendees
|
||||
);
|
||||
return { ...getRecurringSeatedBookingOutput, seatUid };
|
||||
}
|
||||
|
||||
@@ -52,11 +52,7 @@ import {
|
||||
APPLE_CALENDAR,
|
||||
CREDENTIAL_CALENDARS,
|
||||
} from "@calcom/platform-constants";
|
||||
import type {
|
||||
ApiResponse,
|
||||
CalendarBusyTimesInput,
|
||||
CreateCalendarCredentialsInput,
|
||||
} from "@calcom/platform-types";
|
||||
import { ApiResponse, CalendarBusyTimesInput, CreateCalendarCredentialsInput } from "@calcom/platform-types";
|
||||
import type { User } from "@calcom/prisma/client";
|
||||
|
||||
export interface CalendarState {
|
||||
|
||||
+12
@@ -1141,6 +1141,9 @@ describe("Event types Endpoints", () => {
|
||||
disableRecordingForGuests: true,
|
||||
disableRecordingForOrganizer: true,
|
||||
enableAutomaticRecordingForOrganizer: true,
|
||||
enableAutomaticTranscription: true,
|
||||
disableTranscriptionForGuests: true,
|
||||
disableTranscriptionForOrganizer: true,
|
||||
},
|
||||
bookingFields: [
|
||||
nameBookingField,
|
||||
@@ -1274,6 +1277,15 @@ describe("Event types Endpoints", () => {
|
||||
expect(updatedEventType.calVideoSettings?.enableAutomaticRecordingForOrganizer).toEqual(
|
||||
body.calVideoSettings?.enableAutomaticRecordingForOrganizer
|
||||
);
|
||||
expect(updatedEventType.calVideoSettings?.enableAutomaticTranscription).toEqual(
|
||||
body.calVideoSettings?.enableAutomaticTranscription
|
||||
);
|
||||
expect(updatedEventType.calVideoSettings?.disableTranscriptionForGuests).toEqual(
|
||||
body.calVideoSettings?.disableTranscriptionForGuests
|
||||
);
|
||||
expect(updatedEventType.calVideoSettings?.disableTranscriptionForOrganizer).toEqual(
|
||||
body.calVideoSettings?.disableTranscriptionForOrganizer
|
||||
);
|
||||
|
||||
eventType.title = newTitle;
|
||||
eventType.scheduleId = secondSchedule.id;
|
||||
|
||||
@@ -79,13 +79,13 @@ export class AtomsVerificationController {
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async verifyEmailCode(@Body() body: VerifyEmailCodeInput): Promise<VerifyEmailCodeOutput> {
|
||||
const verified = await this.verificationService.verifyEmailCodeUnAuthenticated({
|
||||
await this.verificationService.verifyEmailCodeUnAuthenticated({
|
||||
email: body.email,
|
||||
code: body.code,
|
||||
});
|
||||
|
||||
return {
|
||||
data: { verified },
|
||||
data: { verified: true },
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export class VerificationAtomsService {
|
||||
|
||||
async verifyEmailCodeUnAuthenticated(input: VerifyEmailCodeInput) {
|
||||
try {
|
||||
return await verifyCodeUnAuthenticated(input);
|
||||
return await verifyCodeUnAuthenticated(input.email, input.code);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message === "invalid_code") {
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { NO_AUTH_PROVIDED_MESSAGE } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import {
|
||||
NO_AUTH_PROVIDED_MESSAGE,
|
||||
ONLY_CLIENT_ID_PROVIDED_MESSAGE,
|
||||
} from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
|
||||
export class OptionalApiAuthGuard extends ApiAuthGuard {
|
||||
handleRequest(error: Error, user: any) {
|
||||
// note(Lauris): optional means that auth is not required but if it is invalid then still throw error.
|
||||
const noAuthProvided = error && error.message.includes(NO_AUTH_PROVIDED_MESSAGE);
|
||||
const onlyClientIdProvided = error && error.message.includes(ONLY_CLIENT_ID_PROVIDED_MESSAGE);
|
||||
|
||||
if (onlyClientIdProvided) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (user || noAuthProvided || !error) {
|
||||
return user || null;
|
||||
} else {
|
||||
|
||||
@@ -32,7 +32,7 @@ import { randomString } from "test/utils/randomString";
|
||||
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
import type { PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
|
||||
|
||||
import { ApiAuthGuardRequest } from "./api-auth.strategy";
|
||||
import { ApiAuthGuardRequest, ONLY_CLIENT_ID_PROVIDED_MESSAGE } from "./api-auth.strategy";
|
||||
import { ApiAuthStrategy } from "./api-auth.strategy";
|
||||
|
||||
describe("ApiAuthStrategy", () => {
|
||||
@@ -244,6 +244,29 @@ describe("ApiAuthStrategy", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("should throw 401 if only OAuth ID is provided", async () => {
|
||||
const context: ExecutionContext = {
|
||||
switchToHttp: () => ({
|
||||
getRequest: () => ({
|
||||
headers: {
|
||||
[X_CAL_CLIENT_ID]: `${oAuthClient.id}gibberish`,
|
||||
},
|
||||
get: (key: string) => ({ origin: "http://localhost:3000" }[key]),
|
||||
}),
|
||||
}),
|
||||
} as ExecutionContext;
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
try {
|
||||
await strategy.authenticate(request);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) {
|
||||
expect(error.getStatus()).toEqual(401);
|
||||
expect(error.message).toContain(ONLY_CLIENT_ID_PROVIDED_MESSAGE);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("should throw 401 if OAuth ID is invalid", async () => {
|
||||
const context: ExecutionContext = {
|
||||
switchToHttp: () => ({
|
||||
|
||||
@@ -32,6 +32,9 @@ export type ApiAuthGuardRequest = Request & {
|
||||
export const NO_AUTH_PROVIDED_MESSAGE =
|
||||
"No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as 'x-cal-secret-key' and 'x-cal-client-id' headers";
|
||||
|
||||
export const ONLY_CLIENT_ID_PROVIDED_MESSAGE =
|
||||
"Only 'x-cal-client-id' header provided. Please also provide 'x-cal-secret-key' header or Auth bearer token as 'Authentication' header";
|
||||
|
||||
@Injectable()
|
||||
export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") {
|
||||
private readonly logger = new Logger("ApiAuthStrategy");
|
||||
@@ -117,10 +120,15 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
}
|
||||
|
||||
const noAuthProvided = !oAuthClientId && !oAuthClientSecret && !bearerToken && !nextAuthToken;
|
||||
const onlyClientIdProvided = !!oAuthClientId && !oAuthClientSecret && !bearerToken && !nextAuthToken;
|
||||
if (noAuthProvided) {
|
||||
throw new UnauthorizedException(`ApiAuthStrategy - ${NO_AUTH_PROVIDED_MESSAGE}`);
|
||||
}
|
||||
|
||||
if (onlyClientIdProvided) {
|
||||
throw new UnauthorizedException(`ApiAuthStrategy - ${ONLY_CLIENT_ID_PROVIDED_MESSAGE}`);
|
||||
}
|
||||
|
||||
throw new UnauthorizedException(
|
||||
`ApiAuthStrategy - Invalid authentication method. Please provide one of the allowed methods: ${
|
||||
allowedMethods && allowedMethods.length > 0 ? allowedMethods.join(", ") : "Any supported method"
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
HttpStatus,
|
||||
Logger,
|
||||
Delete,
|
||||
BadRequestException,
|
||||
ParseIntPipe,
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { ApiExcludeController } from "@nestjs/swagger";
|
||||
@@ -51,7 +51,7 @@ export class BillingController {
|
||||
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
|
||||
@MembershipRoles(["OWNER", "ADMIN", "MEMBER"])
|
||||
async checkTeamBilling(
|
||||
@Param("teamId") teamId: number
|
||||
@Param("teamId", ParseIntPipe) teamId: number
|
||||
): Promise<ApiResponse<CheckPlatformBillingResponseDto>> {
|
||||
const { status, plan } = await this.billingService.getBillingData(teamId);
|
||||
|
||||
|
||||
+8
-1
@@ -627,7 +627,7 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
expect(responseBody.data.metadata).toEqual(postResponseData.user.metadata);
|
||||
});
|
||||
|
||||
it(`/PUT/:id`, async () => {
|
||||
it(`/PATCH/:id`, async () => {
|
||||
const userUpdatedEmail = "pineapple-pizza@gmail.com";
|
||||
const body: UpdateManagedUserInput = { email: userUpdatedEmail, locale: Locales.PT_BR };
|
||||
|
||||
@@ -652,6 +652,13 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
const [domainName, TLD] = emailDomain.split(".");
|
||||
expect(responseBody.data.username).toEqual(slugify(`${emailUser}-${domainName}-${TLD}`));
|
||||
expect(responseBody.data.locale).toEqual(Locales.PT_BR);
|
||||
|
||||
const profile = await profilesRepositoryFixture.findByOrgIdUserId(
|
||||
organization.id,
|
||||
responseBody.data.id
|
||||
);
|
||||
expect(profile).toBeDefined();
|
||||
expect(profile?.username).toEqual(responseBody.data.username);
|
||||
});
|
||||
|
||||
it("should force refresh tokens", async () => {
|
||||
|
||||
+9
-2
@@ -1,4 +1,5 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { GetOrgId } from "@/modules/auth/decorators/get-org-id/get-org-id.decorator";
|
||||
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard";
|
||||
@@ -127,12 +128,18 @@ export class OAuthClientUsersController {
|
||||
async updateUser(
|
||||
@Param("clientId") clientId: string,
|
||||
@Param("userId") userId: number,
|
||||
@Body() body: UpdateManagedUserInput
|
||||
@Body() body: UpdateManagedUserInput,
|
||||
@GetOrgId() organizationId: number
|
||||
): Promise<GetManagedUserOutput> {
|
||||
await this.validateManagedUserOwnership(clientId, userId);
|
||||
this.logger.log(`Updating user with ID ${userId}: ${JSON.stringify(body, null, 2)}`);
|
||||
|
||||
const user = await this.oAuthClientUsersService.updateOAuthClientUser(clientId, userId, body);
|
||||
const user = await this.oAuthClientUsersService.updateOAuthClientUser(
|
||||
clientId,
|
||||
userId,
|
||||
body,
|
||||
organizationId
|
||||
);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
|
||||
@@ -21,6 +21,7 @@ import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.se
|
||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||
import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { ProfilesModule } from "@/modules/profiles/profiles.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
@@ -44,6 +45,7 @@ import { JwtService } from "@nestjs/jwt";
|
||||
StripeModule,
|
||||
BillingModule,
|
||||
SchedulesModule_2024_04_15,
|
||||
ProfilesModule,
|
||||
],
|
||||
providers: [
|
||||
OAuthClientRepository,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { Locales } from "@/lib/enums/locales";
|
||||
import { GetManagedUsersInput } from "@/modules/oauth-clients/controllers/oauth-client-users/inputs/get-managed-users.input";
|
||||
import { ProfilesRepository } from "@/modules/profiles/profiles.repository";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||
@@ -21,7 +22,8 @@ export class OAuthClientUsersService {
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly eventTypesService: EventTypesService_2024_04_15,
|
||||
private readonly schedulesService: SchedulesService_2024_04_15,
|
||||
private readonly calendarsService: CalendarsService
|
||||
private readonly calendarsService: CalendarsService,
|
||||
private readonly profilesRepository: ProfilesRepository
|
||||
) {}
|
||||
|
||||
async createOAuthClientUser(oAuthClient: PlatformOAuthClient, body: CreateManagedUserInput) {
|
||||
@@ -130,7 +132,12 @@ export class OAuthClientUsersService {
|
||||
return managedUsers;
|
||||
}
|
||||
|
||||
async updateOAuthClientUser(oAuthClientId: string, userId: number, body: UpdateManagedUserInput) {
|
||||
async updateOAuthClientUser(
|
||||
oAuthClientId: string,
|
||||
userId: number,
|
||||
body: UpdateManagedUserInput,
|
||||
organizationId: number
|
||||
) {
|
||||
if (body.email) {
|
||||
const emailWithOAuthId = OAuthClientUsersService.getOAuthUserEmail(oAuthClientId, body.email);
|
||||
body.email = emailWithOAuthId;
|
||||
@@ -138,6 +145,9 @@ export class OAuthClientUsersService {
|
||||
const [domainName, TLD] = emailDomain.split(".");
|
||||
const newUsername = slugify(`${emailUser}-${domainName}-${TLD}`);
|
||||
await this.userRepository.updateUsername(userId, newUsername);
|
||||
await this.profilesRepository.updateProfile(organizationId, userId, {
|
||||
username: newUsername,
|
||||
});
|
||||
}
|
||||
|
||||
return this.userRepository.update(userId, body);
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ import { plainToClass } from "class-transformer";
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { User } from "@calcom/prisma/client";
|
||||
|
||||
import type { CreateDelegationCredentialInput } from "./inputs/create-delegation-credential.input";
|
||||
import { CreateDelegationCredentialInput } from "./inputs/create-delegation-credential.input";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/organizations/:orgId/delegation-credentials",
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ import type {
|
||||
import type { User, Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Organizations Event Types Endpoints", () => {
|
||||
describe("User Authentication - User is Org Admin", () => {
|
||||
describe("User Authentication - User is Org Admin but not team admin", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
+1238
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,8 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class ProfilesRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService) {}
|
||||
@@ -29,4 +31,16 @@ export class ProfilesRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateProfile(orgId: number, userId: number, body: Prisma.ProfileUpdateInput) {
|
||||
return this.dbRead.prisma.profile.update({
|
||||
where: {
|
||||
userId_organizationId: {
|
||||
userId,
|
||||
organizationId: orgId,
|
||||
},
|
||||
},
|
||||
data: body,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -96,6 +96,9 @@
|
||||
--cal-bg-visualization-7-subtle: hsla(0, 96%, 89%, 1); /* red-100 */
|
||||
--cal-bg-visualization-7-emphasis: hsla(0, 84%, 60%, 1); /* red-500 */
|
||||
|
||||
/* Launch Background */
|
||||
--cal-bg-launch-dark: hsla(242, 53%, 10%, 1); /* #0D0C27 - Launch card dark background */
|
||||
|
||||
/* Legacy Background Components - Consider deprecating */
|
||||
--cal-bg-info: hsla(221, 91%, 93%, 1); /* #dee9fc - info backgrounds */
|
||||
--cal-bg-success: hsla(142, 76%, 94%, 1); /* #e2fbe8 - success backgrounds */
|
||||
@@ -192,6 +195,9 @@
|
||||
--cal-bg-visualization-7-subtle: hsla(0, 70%, 35%, 1); /* red-200 */
|
||||
--cal-bg-visualization-7-emphasis: hsla(0, 84%, 60%, 1); /* red-500 */
|
||||
|
||||
/* Launch Background */
|
||||
--cal-bg-launch-dark: hsla(242, 53%, 10%, 1); /* #0D0C27 - Launch card dark background */
|
||||
|
||||
/* Legacy Background Components - Consider deprecating */
|
||||
--cal-bg-info: hsla(228, 56%, 33%, 1); /* Keeping existing info background */
|
||||
--cal-bg-success: hsla(133, 34%, 24%, 1); /* Keeping existing success background */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils";
|
||||
import { getTeamData } from "@calcom/features/ee/teams/lib/getTeamData";
|
||||
import {
|
||||
getEventTypeHosts,
|
||||
@@ -17,7 +18,6 @@ import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import type { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
|
||||
|
||||
export async function getCachedTeamData(teamSlug: string, orgSlug: string | null) {
|
||||
return unstable_cache(async () => getTeamData(teamSlug, orgSlug), ["team-data", teamSlug, orgSlug ?? ""], {
|
||||
@@ -134,7 +134,7 @@ export async function getCRMData(
|
||||
|
||||
if (!teamMemberEmail || !crmOwnerRecordType || !crmAppSlug) {
|
||||
const { getTeamMemberEmailForResponseOrContactUsingUrlQuery } = await import(
|
||||
"@calcom/lib/server/getTeamMemberEmailFromCrm"
|
||||
"@calcom/features/ee/teams/lib/getTeamMemberEmailFromCrm"
|
||||
);
|
||||
const {
|
||||
email,
|
||||
|
||||
+4
-4
@@ -6,6 +6,8 @@ import { Controller, useFieldArray, useWatch } from "react-hook-form";
|
||||
import { Toaster } from "sonner";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { FieldTypes } from "@calcom/app-store/routing-forms/lib/FieldTypes";
|
||||
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
@@ -19,13 +21,11 @@ import {
|
||||
} from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
import type { getServerSidePropsForSingleFormView as getServerSideProps } from "@calcom/web/lib/apps/routing-forms/[...pages]/getServerSidePropsSingleForm";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import SingleForm from "../../components/SingleForm";
|
||||
import type { getServerSidePropsForSingleFormView as getServerSideProps } from "../../components/getServerSidePropsSingleForm";
|
||||
import { FieldTypes } from "../../lib/FieldTypes";
|
||||
import type { RoutingFormWithResponseCount } from "../../types/types";
|
||||
import SingleForm from "@components/apps/routing-forms/SingleForm";
|
||||
|
||||
type HookForm = UseFormReturn<RoutingFormWithResponseCount>;
|
||||
|
||||
+3
-3
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import type z from "zod";
|
||||
|
||||
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
|
||||
import { WhenToWriteToRecord, SalesforceFieldType } from "@calcom/app-store/salesforce/lib/enums";
|
||||
import type { writeToRecordDataSchema as salesforceWriteToRecordDataSchema } from "@calcom/app-store/salesforce/zod";
|
||||
import { routingFormIncompleteBookingDataSchema as salesforceRoutingFormIncompleteBookingDataSchema } from "@calcom/app-store/salesforce/zod";
|
||||
@@ -17,10 +18,9 @@ import { Select } from "@calcom/ui/components/form";
|
||||
import { Label } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import type { getServerSidePropsForSingleFormView as getServerSideProps } from "@calcom/web/lib/apps/routing-forms/[...pages]/getServerSidePropsSingleForm";
|
||||
|
||||
import SingleForm from "../../components/SingleForm";
|
||||
import type { getServerSidePropsForSingleFormView as getServerSideProps } from "../../components/getServerSidePropsSingleForm";
|
||||
import type { RoutingFormWithResponseCount } from "../../types/types";
|
||||
import SingleForm from "@components/apps/routing-forms/SingleForm";
|
||||
|
||||
function Page({ form }: { form: RoutingFormWithResponseCount }) {
|
||||
const { t } = useLocale();
|
||||
+33
-33
@@ -10,9 +10,39 @@ import type { UseFormReturn } from "react-hook-form";
|
||||
import { Toaster } from "sonner";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { routingFormAppComponents } from "@calcom/app-store/routing-forms/appComponents";
|
||||
import DynamicAppComponent from "@calcom/app-store/routing-forms/components/DynamicAppComponent";
|
||||
import { EmptyState } from "@calcom/app-store/routing-forms/components/_components/EmptyState";
|
||||
import { RoutingSkeleton } from "@calcom/app-store/routing-forms/components/_components/RoutingSkeleton";
|
||||
import {
|
||||
withRaqbSettingsAndWidgets,
|
||||
ConfigFor,
|
||||
} from "@calcom/app-store/routing-forms/components/react-awesome-query-builder/config/uiConfig";
|
||||
import { RoutingPages } from "@calcom/app-store/routing-forms/lib/RoutingPages";
|
||||
import { createFallbackRoute } from "@calcom/app-store/routing-forms/lib/createFallbackRoute";
|
||||
import getEventTypeAppMetadata from "@calcom/app-store/routing-forms/lib/getEventTypeAppMetadata";
|
||||
import {
|
||||
getQueryBuilderConfigForFormFields,
|
||||
getQueryBuilderConfigForAttributes,
|
||||
type FormFieldsQueryBuilderConfigWithRaqbFields,
|
||||
type AttributesQueryBuilderConfigWithRaqbFields,
|
||||
isDynamicOperandField,
|
||||
} from "@calcom/app-store/routing-forms/lib/getQueryBuilderConfig";
|
||||
import isRouter from "@calcom/app-store/routing-forms/lib/isRouter";
|
||||
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
|
||||
import type {
|
||||
GlobalRoute,
|
||||
LocalRoute,
|
||||
SerializableRoute,
|
||||
Attribute,
|
||||
EditFormRoute,
|
||||
AttributeRoutingConfig,
|
||||
} from "@calcom/app-store/routing-forms/types/types";
|
||||
import type { zodRoutes } from "@calcom/app-store/routing-forms/zod";
|
||||
import { RouteActionType } from "@calcom/app-store/routing-forms/zod";
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import type { EventTypesByViewer } from "@calcom/features/eventtypes/lib/getEventTypesByViewer";
|
||||
import { areTheySiblingEntities } from "@calcom/lib/entityPermissionUtils.shared";
|
||||
import type { EventTypesByViewer } from "@calcom/lib/event-types/getEventTypesByViewer";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { buildEmptyQueryValue, raqbQueryValueUtils } from "@calcom/lib/raqb/raqbUtils";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
@@ -29,39 +59,9 @@ import { SelectField } from "@calcom/ui/components/form";
|
||||
import { Switch } from "@calcom/ui/components/form";
|
||||
import type { IconName } from "@calcom/ui/components/icon";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import type { getServerSidePropsForSingleFormView as getServerSideProps } from "@calcom/web/lib/apps/routing-forms/[...pages]/getServerSidePropsSingleForm";
|
||||
|
||||
import { routingFormAppComponents } from "../../appComponents";
|
||||
import DynamicAppComponent from "../../components/DynamicAppComponent";
|
||||
import SingleForm from "../../components/SingleForm";
|
||||
import { EmptyState } from "../../components/_components/EmptyState";
|
||||
import { RoutingSkeleton } from "../../components/_components/RoutingSkeleton";
|
||||
import type { getServerSidePropsForSingleFormView as getServerSideProps } from "../../components/getServerSidePropsSingleForm";
|
||||
import {
|
||||
withRaqbSettingsAndWidgets,
|
||||
ConfigFor,
|
||||
} from "../../components/react-awesome-query-builder/config/uiConfig";
|
||||
import { RoutingPages } from "../../lib/RoutingPages";
|
||||
import { createFallbackRoute } from "../../lib/createFallbackRoute";
|
||||
import getEventTypeAppMetadata from "../../lib/getEventTypeAppMetadata";
|
||||
import {
|
||||
getQueryBuilderConfigForFormFields,
|
||||
getQueryBuilderConfigForAttributes,
|
||||
type FormFieldsQueryBuilderConfigWithRaqbFields,
|
||||
type AttributesQueryBuilderConfigWithRaqbFields,
|
||||
isDynamicOperandField,
|
||||
} from "../../lib/getQueryBuilderConfig";
|
||||
import isRouter from "../../lib/isRouter";
|
||||
import type { RoutingFormWithResponseCount } from "../../types/types";
|
||||
import type {
|
||||
GlobalRoute,
|
||||
LocalRoute,
|
||||
SerializableRoute,
|
||||
Attribute,
|
||||
EditFormRoute,
|
||||
AttributeRoutingConfig,
|
||||
} from "../../types/types";
|
||||
import type { zodRoutes } from "../../zod";
|
||||
import { RouteActionType } from "../../zod";
|
||||
import SingleForm from "@components/apps/routing-forms/SingleForm";
|
||||
|
||||
type Form = inferSSRProps<typeof getServerSideProps>["form"];
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useRedirectToLoginIfUnauthenticated } from "@calcom/features/auth/lib/hooks/useRedirectToLoginIfUnauthenticated";
|
||||
|
||||
export function RoutingFormAuthGuard({ children }: { children: React.ReactNode }) {
|
||||
useRedirectToLoginIfUnauthenticated();
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
+9
-10
@@ -6,7 +6,15 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import FormInputFields from "@calcom/app-store/routing-forms/components/FormInputFields";
|
||||
import { getAbsoluteEventTypeRedirectUrlWithEmbedSupport } from "@calcom/app-store/routing-forms/getEventTypeRedirectUrl";
|
||||
import getFieldIdentifier from "@calcom/app-store/routing-forms/lib/getFieldIdentifier";
|
||||
import { findMatchingRoute } from "@calcom/app-store/routing-forms/lib/processRoute";
|
||||
import { substituteVariables } from "@calcom/app-store/routing-forms/lib/substituteVariables";
|
||||
import { getFieldResponseForJsonLogic } from "@calcom/app-store/routing-forms/lib/transformResponse";
|
||||
import type { NonRouterRoute, FormResponse } from "@calcom/app-store/routing-forms/types/types";
|
||||
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { getUrlSearchParamsToForward } from "@calcom/features/routing-forms/lib/getUrlSearchParamsToForward";
|
||||
import useGetBrandingColours from "@calcom/lib/getBrandColours";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -18,16 +26,7 @@ import classNames from "@calcom/ui/classNames";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { useCalcomTheme } from "@calcom/ui/styles";
|
||||
|
||||
import FormInputFields from "../../components/FormInputFields";
|
||||
import { getAbsoluteEventTypeRedirectUrlWithEmbedSupport } from "../../getEventTypeRedirectUrl";
|
||||
import getFieldIdentifier from "../../lib/getFieldIdentifier";
|
||||
import { findMatchingRoute } from "../../lib/processRoute";
|
||||
import { substituteVariables } from "../../lib/substituteVariables";
|
||||
import { getFieldResponseForJsonLogic } from "../../lib/transformResponse";
|
||||
import type { NonRouterRoute, FormResponse } from "../../types/types";
|
||||
import type { getServerSideProps } from "./getServerSideProps";
|
||||
import { getUrlSearchParamsToForward } from "./getUrlSearchParamsToForward";
|
||||
import type { getServerSideProps } from "@calcom/web/lib/apps/routing-forms/[...pages]/getServerSidePropsRoutingLink";
|
||||
|
||||
type Props = inferSSRProps<typeof getServerSideProps>;
|
||||
const useBrandColors = ({
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export const routingFormsComponents = {
|
||||
"form-edit": () => import("./FormEdit").then((mod) => mod.default),
|
||||
"route-builder": () => import("./RouteBuilder").then((mod) => mod.default),
|
||||
"routing-link": () => import("./RoutingLink").then((mod) => mod.default),
|
||||
"incomplete-booking": () => import("./IncompleteBooking").then((mod) => mod.default),
|
||||
};
|
||||
@@ -1,7 +1,22 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import FormProvider from "./FormProvider";
|
||||
import { RoutingFormAuthGuard } from "./RoutingFormAuthGuard";
|
||||
|
||||
export default async function Layout({ children }: { children: ReactNode }) {
|
||||
return <FormProvider>{children}</FormProvider>;
|
||||
interface LayoutProps {
|
||||
children: ReactNode;
|
||||
params: { pages: string[] };
|
||||
}
|
||||
|
||||
export default async function Layout({ children, params }: LayoutProps) {
|
||||
const isPublic = params.pages?.[0] === "routing-link";
|
||||
if (isPublic) {
|
||||
return <FormProvider>{children}</FormProvider>;
|
||||
}
|
||||
|
||||
return (
|
||||
<RoutingFormAuthGuard>
|
||||
<FormProvider>{children}</FormProvider>
|
||||
</RoutingFormAuthGuard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import type { PageProps as ServerPageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { routingFormsComponents } from "@calcom/app-store/routing-forms/pages/app-routing.client-config";
|
||||
import type { routingServerSidePropsConfig } from "@calcom/app-store/routing-forms/pages/app-routing.server-config";
|
||||
|
||||
import type { routingServerSidePropsConfig } from "@lib/apps/routing-forms/[...pages]/app-routing.server-config";
|
||||
import { getServerSideProps } from "@lib/apps/routing-forms/[...pages]/getServerSideProps";
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { routingFormsComponents } from "./app-routing.client-config";
|
||||
|
||||
const normalizePages = (pages: string[] | string | undefined) => {
|
||||
const normalizedPages = Array.isArray(pages) ? pages : pages?.split("/") ?? [];
|
||||
return normalizedPages[0];
|
||||
|
||||
+11
-5
@@ -4,6 +4,8 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { isFallbackRoute } from "@calcom/app-store/routing-forms/lib/isFallbackRoute";
|
||||
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import SkeletonLoaderTeamList from "@calcom/features/ee/teams/components/SkeletonloaderTeamList";
|
||||
import { CreateButtonWithTeamsList } from "@calcom/features/ee/teams/components/createButton/CreateButtonWithTeamsList";
|
||||
@@ -25,11 +27,15 @@ import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { List, ListLinkItem } from "@calcom/ui/components/list";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
|
||||
import type { SetNewFormDialogState, NewFormDialogState } from "../../components/FormActions";
|
||||
import { FormAction, FormActionsDropdown, FormActionsProvider } from "../../components/FormActions";
|
||||
import { isFallbackRoute } from "../../lib/isFallbackRoute";
|
||||
import type { RoutingFormWithResponseCount } from "../../types/types";
|
||||
import type {
|
||||
SetNewFormDialogState,
|
||||
NewFormDialogState,
|
||||
} from "@calcom/web/components/apps/routing-forms/FormActions";
|
||||
import {
|
||||
FormAction,
|
||||
FormActionsDropdown,
|
||||
FormActionsProvider,
|
||||
} from "@calcom/web/components/apps/routing-forms/FormActions";
|
||||
|
||||
function NewFormButton({ setNewFormDialogState }: { setNewFormDialogState: SetNewFormDialogState }) {
|
||||
const { t } = useLocale();
|
||||
@@ -1,6 +1,6 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import Forms from "@calcom/app-store/routing-forms/pages/forms/[...appPages]";
|
||||
import Forms from "./Forms";
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Promise<{ pages: string[] }> }) => {
|
||||
const pages = (await params).pages;
|
||||
|
||||
@@ -8,6 +8,10 @@ import { z } from "zod";
|
||||
|
||||
import { EventTypeWebWrapper } from "@calcom/atoms/event-types/wrappers/EventTypeWebWrapper";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { eventTypesRouter } from "@calcom/trpc/server/routers/viewer/eventTypes/_router";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
@@ -40,6 +44,95 @@ const getCachedEventType = unstable_cache(
|
||||
{ revalidate: 3600 } // Cache for 1 hour
|
||||
);
|
||||
|
||||
const getEventPermissions = async (userId: number, teamId: number | null) => {
|
||||
// Personal event - has all perms
|
||||
if (!teamId)
|
||||
return {
|
||||
eventTypes: {
|
||||
canRead: true,
|
||||
canCreate: true,
|
||||
canUpdate: true,
|
||||
canDelete: true,
|
||||
},
|
||||
workflows: {
|
||||
canRead: true,
|
||||
canCreate: true,
|
||||
canUpdate: true,
|
||||
canDelete: true,
|
||||
},
|
||||
};
|
||||
|
||||
const membership = await prisma.membership.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!membership) throw new Error("Membership not found");
|
||||
|
||||
const [eventTypePermissions, workflowPermissions] = await Promise.all([
|
||||
getResourcePermissions({
|
||||
userId,
|
||||
teamId,
|
||||
resource: Resource.EventType,
|
||||
userRole: membership.role,
|
||||
fallbackRoles: {
|
||||
read: {
|
||||
roles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
},
|
||||
update: {
|
||||
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
},
|
||||
delete: {
|
||||
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
},
|
||||
create: {
|
||||
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
},
|
||||
},
|
||||
}),
|
||||
getResourcePermissions({
|
||||
userId,
|
||||
teamId,
|
||||
resource: Resource.Workflow,
|
||||
userRole: membership.role,
|
||||
fallbackRoles: {
|
||||
read: {
|
||||
roles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
},
|
||||
update: {
|
||||
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
},
|
||||
delete: {
|
||||
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
},
|
||||
create: {
|
||||
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
eventTypes: {
|
||||
canRead: eventTypePermissions.canRead,
|
||||
canCreate: eventTypePermissions.canCreate,
|
||||
canUpdate: eventTypePermissions.canEdit,
|
||||
canDelete: eventTypePermissions.canDelete,
|
||||
},
|
||||
workflows: {
|
||||
canRead: workflowPermissions.canRead,
|
||||
canCreate: workflowPermissions.canCreate,
|
||||
canUpdate: workflowPermissions.canEdit,
|
||||
canDelete: workflowPermissions.canDelete,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ServerPage = async ({ params }: PageProps) => {
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
if (!session?.user?.id) {
|
||||
@@ -59,7 +152,10 @@ const ServerPage = async ({ params }: PageProps) => {
|
||||
throw new Error("This event type does not exist");
|
||||
}
|
||||
|
||||
return <EventTypeWebWrapper data={data} id={eventTypeId} />;
|
||||
// Fetch permissions for the event type's team
|
||||
const permissions = await getEventPermissions(session.user.id, data.eventType.teamId);
|
||||
|
||||
return <EventTypeWebWrapper data={data} id={eventTypeId} permissions={permissions} />;
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import InsightsCallHistoryPage from "~/insights/insights-call-history-view";
|
||||
|
||||
import { checkInsightsPagePermission } from "../checkInsightsPagePermission";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("call_history"),
|
||||
(t) => t("call_history_subtitle"),
|
||||
undefined,
|
||||
undefined,
|
||||
"/insights/call-history"
|
||||
);
|
||||
|
||||
export default async function Page() {
|
||||
await checkInsightsPagePermission();
|
||||
|
||||
return <InsightsCallHistoryPage />;
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { redirect } from "next/navigation";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { hasInsightsPermission } from "@calcom/features/insights/server/hasInsightsPermission";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
@@ -21,14 +20,5 @@ export async function checkInsightsPagePermission() {
|
||||
redirect("/auth/login");
|
||||
}
|
||||
|
||||
const hasPermission = await hasInsightsPermission({
|
||||
userId: session.user.id,
|
||||
organizationId: session.user.org?.id,
|
||||
});
|
||||
|
||||
if (!hasPermission) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
+8
-13
@@ -5,37 +5,34 @@ import dynamic from "next/dynamic";
|
||||
import type { FC } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { getPaymentAppData } from "@calcom/app-store/_utils/payments/getPaymentAppData";
|
||||
import { getSuccessPageLocationMessage } from "@calcom/app-store/locations";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { PayIcon } from "@calcom/features/bookings/components/event-meta/PayIcon";
|
||||
import { Price } from "@calcom/features/bookings/components/event-meta/Price";
|
||||
import type { PaymentPageProps } from "@calcom/features/ee/payments/pages/payment";
|
||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { getPaymentAppData } from "@calcom/lib/getPaymentAppData";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
|
||||
import { CURRENT_TIMEZONE } from "@calcom/lib/timezoneConstants";
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
|
||||
import type { PaymentPageProps } from "../pages/payment";
|
||||
|
||||
const StripePaymentComponent = dynamic(() => import("./Payment"), {
|
||||
const StripePaymentComponent = dynamic(() => import("@calcom/features/ee/payments/components/Payment"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const PaypalPaymentComponent = dynamic(
|
||||
() =>
|
||||
import("@calcom/app-store/paypal/components/PaypalPaymentComponent").then(
|
||||
(m) => m.PaypalPaymentComponent
|
||||
),
|
||||
import("@calcom/web/components/apps/paypal/PaypalPaymentComponent").then((m) => m.PaypalPaymentComponent),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const AlbyPaymentComponent = dynamic(
|
||||
() => import("@calcom/app-store/alby/components/AlbyPaymentComponent").then((m) => m.AlbyPaymentComponent),
|
||||
() => import("@calcom/web/components/apps/alby/AlbyPaymentComponent").then((m) => m.AlbyPaymentComponent),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
@@ -43,9 +40,7 @@ const AlbyPaymentComponent = dynamic(
|
||||
|
||||
const HitpayPaymentComponent = dynamic(
|
||||
() =>
|
||||
import("@calcom/app-store/hitpay/components/HitpayPaymentComponent").then(
|
||||
(m) => m.HitpayPaymentComponent
|
||||
),
|
||||
import("@calcom/web/components/apps/hitpay/HitpayPaymentComponent").then((m) => m.HitpayPaymentComponent),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
@@ -53,7 +48,7 @@ const HitpayPaymentComponent = dynamic(
|
||||
|
||||
const BtcpayPaymentComponent = dynamic(
|
||||
() =>
|
||||
import("@calcom/app-store/btcpayserver/components/BtcpayPaymentComponent").then(
|
||||
import("@calcom/web/components/apps/btcpayserver/BtcpayPaymentComponent").then(
|
||||
(m) => m.BtcpayPaymentComponent
|
||||
),
|
||||
{
|
||||
@@ -146,7 +141,7 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
|
||||
<div className="col-span-2 mb-6 font-semibold">
|
||||
<Price
|
||||
currency={paymentAppData.currency}
|
||||
price={paymentAppData.price}
|
||||
price={props.payment?.amount ?? paymentAppData.price}
|
||||
displayAlternateSymbol={false}
|
||||
/>
|
||||
</div>
|
||||
@@ -3,12 +3,13 @@ import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import PaymentPage from "@calcom/features/ee/payments/components/PaymentPage";
|
||||
import { getServerSideProps, type PaymentPageProps } from "@calcom/features/ee/payments/pages/payment";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
import PaymentPage from "./PaymentPage";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const props = await getData(
|
||||
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
|
||||
|
||||
+1
-3
@@ -6,7 +6,6 @@ import { redirect } from "next/navigation";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import WebhooksView from "@calcom/features/webhooks/pages/webhooks-view";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||
import { webhookRouter } from "@calcom/trpc/server/routers/viewer/webhook/_router";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
@@ -26,11 +25,10 @@ const WebhooksViewServerWrapper = async () => {
|
||||
redirect("/auth/login");
|
||||
}
|
||||
|
||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
||||
const caller = await createRouterCaller(webhookRouter);
|
||||
const data = await caller.getByViewer();
|
||||
|
||||
return <WebhooksView data={data} isAdmin={isAdmin} />;
|
||||
return <WebhooksView data={data} />;
|
||||
};
|
||||
|
||||
export default WebhooksViewServerWrapper;
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ const Page = async () => {
|
||||
<SettingsHeader
|
||||
title={t("billing")}
|
||||
description={t("manage_billing_description")}
|
||||
borderInShellHeader={true}>
|
||||
borderInShellHeader={false}>
|
||||
<BillingView />
|
||||
</SettingsHeader>
|
||||
);
|
||||
|
||||
+6
-6
@@ -107,9 +107,9 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
|
||||
|
||||
const { isAdvancedMode, permissions, color } = form.watch();
|
||||
|
||||
const filteredResources = useMemo(() => {
|
||||
const { filteredResources, scopedRegistry } = useMemo(() => {
|
||||
const scopedRegistry = getPermissionsForScope(scope);
|
||||
return Object.keys(scopedRegistry).filter((resource) =>
|
||||
const filteredResources = Object.keys(scopedRegistry).filter((resource) =>
|
||||
t(
|
||||
scopedRegistry[resource as Resource][CrudAction.All as keyof (typeof scopedRegistry)[Resource]]
|
||||
?.i18nKey || ""
|
||||
@@ -117,6 +117,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
|
||||
.toLowerCase()
|
||||
.includes(searchQuery.toLowerCase())
|
||||
);
|
||||
return { filteredResources, scopedRegistry };
|
||||
}, [searchQuery, t, scope]);
|
||||
|
||||
const createMutation = trpc.viewer.pbac.createRole.useMutation({
|
||||
@@ -212,7 +213,6 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
|
||||
disabled={isSystemRole}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{filteredResources.map((resource) => (
|
||||
<AdvancedPermissionGroup
|
||||
key={resource}
|
||||
@@ -222,7 +222,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
|
||||
disabled={isSystemRole}
|
||||
scope={scope}
|
||||
/>
|
||||
))}
|
||||
))}{" "}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-muted rounded-xl p-1">
|
||||
@@ -237,7 +237,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-default border-subtle divide-subtle divide-y rounded-[10px] border">
|
||||
{Object.keys(getPermissionsForScope(scope)).map((resource) => (
|
||||
{Object.keys(scopedRegistry).map((resource) => (
|
||||
<SimplePermissionItem
|
||||
key={resource}
|
||||
resource={resource}
|
||||
@@ -247,7 +247,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
|
||||
scope={scope}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>{" "}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+10
-4
@@ -1,7 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import type { Resource, Scope } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { PERMISSION_REGISTRY } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import type { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import {
|
||||
Scope,
|
||||
PERMISSION_REGISTRY,
|
||||
getPermissionsForScope,
|
||||
} from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { ToggleGroup } from "@calcom/ui/components/form";
|
||||
|
||||
@@ -21,11 +25,13 @@ export function SimplePermissionItem({
|
||||
permissions,
|
||||
onChange,
|
||||
disabled,
|
||||
scope,
|
||||
scope = Scope.Organization,
|
||||
}: SimplePermissionItemProps) {
|
||||
const { t } = useLocale();
|
||||
const { getResourcePermissionLevel, toggleResourcePermissionLevel } = usePermissions(scope);
|
||||
const scopedRegistry = getPermissionsForScope(scope);
|
||||
|
||||
const registry = scopedRegistry || PERMISSION_REGISTRY;
|
||||
const isAllResources = resource === "*";
|
||||
const options = isAllResources
|
||||
? [
|
||||
@@ -41,7 +47,7 @@ export function SimplePermissionItem({
|
||||
return (
|
||||
<div className="flex items-center justify-between px-3 py-2">
|
||||
<span className="text-default text-sm font-medium leading-none">
|
||||
{t(PERMISSION_REGISTRY[resource as Resource]._resource?.i18nKey || resource)}
|
||||
{t(registry[resource as Resource]._resource?.i18nKey || resource)}
|
||||
</span>
|
||||
<ToggleGroup
|
||||
onValueChange={(val) => {
|
||||
|
||||
+17
-10
@@ -1,8 +1,5 @@
|
||||
import { CrudAction, Scope } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import {
|
||||
PERMISSION_REGISTRY,
|
||||
getPermissionsForScope,
|
||||
} from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { getPermissionsForScope } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import {
|
||||
getTransitiveDependencies,
|
||||
getTransitiveDependents,
|
||||
@@ -52,7 +49,8 @@ export function usePermissions(scope: Scope = Scope.Organization): UsePermission
|
||||
return permissions.includes("*.*") ? "all" : "none";
|
||||
}
|
||||
|
||||
const resourceConfig = PERMISSION_REGISTRY[resource as keyof typeof PERMISSION_REGISTRY];
|
||||
const scopedRegistry = getPermissionsForScope(scope);
|
||||
const resourceConfig = scopedRegistry[resource as keyof typeof scopedRegistry];
|
||||
if (!resourceConfig) return "none";
|
||||
|
||||
// Check if global all permissions (*.*) is present
|
||||
@@ -111,7 +109,19 @@ export function usePermissions(scope: Scope = Scope.Organization): UsePermission
|
||||
allResourcePerms = Object.keys(resourceConfig)
|
||||
.filter((action) => !action.startsWith("_"))
|
||||
.map((action) => `${resource}.${action}`);
|
||||
|
||||
// Add the resource permissions
|
||||
newPermissions.push(...allResourcePerms);
|
||||
|
||||
// Add all transitive dependencies for each permission
|
||||
allResourcePerms.forEach((perm) => {
|
||||
const dependencies = getTransitiveDependencies(perm, scope);
|
||||
dependencies.forEach((dep) => {
|
||||
if (!newPermissions.includes(dep)) {
|
||||
newPermissions.push(dep);
|
||||
}
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -132,15 +142,12 @@ export function usePermissions(scope: Scope = Scope.Organization): UsePermission
|
||||
// First, remove *.* since we're modifying individual permissions
|
||||
let newPermissions = currentPermissions.filter((p) => p !== "*.*");
|
||||
|
||||
// Parse the permission to get resource and action
|
||||
const [resource, action] = permission.split(".");
|
||||
|
||||
if (enabled) {
|
||||
// Add the requested permission
|
||||
newPermissions.push(permission);
|
||||
|
||||
// Add all transitive dependencies
|
||||
const dependencies = getTransitiveDependencies(permission);
|
||||
const dependencies = getTransitiveDependencies(permission, scope);
|
||||
dependencies.forEach((dependency) => {
|
||||
if (!newPermissions.includes(dependency)) {
|
||||
newPermissions.push(dependency);
|
||||
@@ -151,7 +158,7 @@ export function usePermissions(scope: Scope = Scope.Organization): UsePermission
|
||||
newPermissions = newPermissions.filter((p) => p !== permission);
|
||||
|
||||
// Remove all transitive dependents
|
||||
const dependents = getTransitiveDependents(permission);
|
||||
const dependents = getTransitiveDependents(permission, scope);
|
||||
dependents.forEach((dependent) => {
|
||||
newPermissions = newPermissions.filter((p) => p !== dependent);
|
||||
});
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import { Resource, CrudAction, Scope } from "@calcom/features/pbac/domain/types/
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { RoleService } from "@calcom/features/pbac/services/role.service";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { getTeamWithMembers } from "@calcom/features/ee/teams/lib/queries";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { SENDER_NAME } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Workflow } from "@calcom/prisma/client";
|
||||
import {
|
||||
MembershipRole,
|
||||
TimeUnit,
|
||||
WorkflowActions,
|
||||
WorkflowTemplates,
|
||||
WorkflowTriggerEvents,
|
||||
} from "@calcom/prisma/enums";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
const WORKFLOW_TEMPLATES = {
|
||||
"wf-10": {
|
||||
name: "Cal.ai No-show Follow-up Call",
|
||||
trigger: WorkflowTriggerEvents.BOOKING_NO_SHOW_UPDATED,
|
||||
time: 0,
|
||||
timeUnit: TimeUnit.MINUTE,
|
||||
},
|
||||
"wf-11": {
|
||||
name: "Cal.ai 1-hour Meeting Reminder",
|
||||
trigger: WorkflowTriggerEvents.BEFORE_EVENT,
|
||||
time: 1,
|
||||
timeUnit: TimeUnit.HOUR,
|
||||
},
|
||||
} as const;
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{
|
||||
action?: string;
|
||||
templateWorkflowId?: string;
|
||||
teamId?: string;
|
||||
name?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const Page = async ({ searchParams }: PageProps) => {
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
const user = session?.user;
|
||||
|
||||
const { action, templateWorkflowId, teamId, name } = await searchParams;
|
||||
|
||||
await checkRateLimitAndThrowError({
|
||||
rateLimitingType: "core",
|
||||
identifier: `createWorkflow:${user?.id}`,
|
||||
});
|
||||
|
||||
if (!user?.id) {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (action) queryParams.set("action", action);
|
||||
if (templateWorkflowId) queryParams.set("templateWorkflowId", templateWorkflowId);
|
||||
if (teamId) queryParams.set("teamId", teamId);
|
||||
if (name) queryParams.set("name", name);
|
||||
|
||||
const callbackUrl = `/workflow/new${queryParams.toString() ? `?${queryParams.toString()}` : ""}`;
|
||||
redirect(`/auth/login?callbackUrl=${encodeURIComponent(callbackUrl)}`);
|
||||
}
|
||||
|
||||
if (action !== "calAi" || !templateWorkflowId) {
|
||||
redirect("/workflows");
|
||||
}
|
||||
|
||||
if (templateWorkflowId !== "wf-10" && templateWorkflowId !== "wf-11") {
|
||||
redirect("/workflows");
|
||||
}
|
||||
|
||||
const userId = user.id;
|
||||
const parsedTeamId = teamId ? parseInt(teamId, 10) : undefined;
|
||||
|
||||
if (parsedTeamId) {
|
||||
const permissionService = new PermissionCheckService();
|
||||
|
||||
const hasPermission = await permissionService.checkPermission({
|
||||
userId,
|
||||
teamId: parsedTeamId,
|
||||
permission: "workflow.create",
|
||||
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
});
|
||||
|
||||
if (!hasPermission) {
|
||||
redirect("/workflows?error=unauthorized");
|
||||
}
|
||||
}
|
||||
|
||||
const template = WORKFLOW_TEMPLATES[templateWorkflowId as keyof typeof WORKFLOW_TEMPLATES];
|
||||
|
||||
try {
|
||||
const workflow: Workflow = await prisma.workflow.create({
|
||||
data: {
|
||||
name: name || template.name,
|
||||
trigger: template.trigger,
|
||||
time: template.time,
|
||||
timeUnit: template.timeUnit,
|
||||
userId,
|
||||
teamId: parsedTeamId,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.workflowStep.create({
|
||||
data: {
|
||||
stepNumber: 1,
|
||||
action: WorkflowActions.CAL_AI_PHONE_CALL,
|
||||
template: WorkflowTemplates.CUSTOM,
|
||||
workflowId: workflow.id,
|
||||
sender: SENDER_NAME,
|
||||
numberVerificationPending: false,
|
||||
verifiedAt: new Date(),
|
||||
includeCalendarEvent: false,
|
||||
},
|
||||
});
|
||||
|
||||
const redirectUrl = `/workflows/${workflow.id}?autoCreateAgent=true&templateWorkflowId=${templateWorkflowId}`;
|
||||
|
||||
redirect(redirectUrl);
|
||||
} catch (error) {
|
||||
if (
|
||||
error &&
|
||||
typeof error === "object" &&
|
||||
"digest" in error &&
|
||||
typeof error.digest === "string" &&
|
||||
error.digest.startsWith("NEXT_REDIRECT")
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.error("Failed to create Cal AI workflow:", error);
|
||||
redirect("/workflows?error=failed-to-create-workflow");
|
||||
}
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -4,8 +4,8 @@ import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { validPassword } from "@calcom/features/auth/lib/validPassword";
|
||||
import { hashPassword } from "@calcom/lib/auth/hashPassword";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import z from "zod";
|
||||
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
|
||||
import { hashPassword } from "@calcom/lib/auth/hashPassword";
|
||||
import { isPasswordValid } from "@calcom/lib/auth/isPasswordValid";
|
||||
import { emailRegex } from "@calcom/lib/emailSchema";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { CalendarCache } from "@calcom/features/calendar-cache/calendar-cache";
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/lib/CalendarManager";
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/features/calendars/lib/CalendarManager";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import notEmpty from "@calcom/lib/notEmpty";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
|
||||
@@ -17,7 +17,7 @@ import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import {
|
||||
getAllTranscriptsAccessLinkFromMeetingId,
|
||||
submitBatchProcessorTranscriptionJob,
|
||||
} from "@calcom/lib/videoClient";
|
||||
} from "@calcom/app-store/videoClient";
|
||||
import { generateVideoToken } from "@calcom/lib/videoTokens";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { getBooking } from "@calcom/web/lib/daily-webhook/getBooking";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { describe, expect, test, vi, afterEach } from "vitest";
|
||||
|
||||
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/lib/videoClient";
|
||||
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/app-store/videoClient";
|
||||
import { verifyVideoToken } from "@calcom/lib/videoTokens";
|
||||
|
||||
import { GET } from "../route";
|
||||
|
||||
vi.mock("@calcom/lib/videoClient", () => ({
|
||||
vi.mock("@calcom/app-store/videoClient", () => ({
|
||||
getDownloadLinkOfCalVideoByRecordingId: vi.fn(),
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/lib/videoClient";
|
||||
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/app-store/videoClient";
|
||||
import { verifyVideoToken } from "@calcom/lib/videoTokens";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
|
||||
@@ -2,8 +2,9 @@ import type { NextRequest } from "next/server";
|
||||
import { Retell } from "retell-sdk";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
import { PrismaAgentRepository } from "@calcom/lib/server/repository/PrismaAgentRepository";
|
||||
import { PrismaPhoneNumberRepository } from "@calcom/lib/server/repository/PrismaPhoneNumberRepository";
|
||||
import type { CalAiPhoneNumber, User, Team } from "@calcom/prisma/client";
|
||||
import type { CalAiPhoneNumber, User, Team, Agent } from "@calcom/prisma/client";
|
||||
|
||||
import { POST } from "../route";
|
||||
|
||||
@@ -25,6 +26,7 @@ type RetellWebhookBody = {
|
||||
from_number?: string;
|
||||
to_number?: string;
|
||||
direction?: "inbound" | "outbound";
|
||||
call_type?: string;
|
||||
call_status?: string;
|
||||
start_timestamp?: number;
|
||||
end_timestamp?: number;
|
||||
@@ -85,6 +87,12 @@ vi.mock("@calcom/lib/server/repository/PrismaPhoneNumberRepository", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/server/repository/PrismaAgentRepository", () => ({
|
||||
PrismaAgentRepository: {
|
||||
findByProviderAgentId: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("next/server", () => ({
|
||||
NextResponse: {
|
||||
json: vi.fn((data, options) => ({
|
||||
@@ -683,4 +691,179 @@ describe("Retell AI Webhook Handler", () => {
|
||||
expect(data.message).toContain("Error charging credits for Retell AI call");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Web Call Tests", () => {
|
||||
const mockAgent: Pick<
|
||||
Agent,
|
||||
"id" | "name" | "providerAgentId" | "enabled" | "userId" | "teamId" | "createdAt" | "updatedAt"
|
||||
> = {
|
||||
id: "agent-123",
|
||||
name: "Test Agent",
|
||||
providerAgentId: "agent_5e3e0d29d692172c2c24d8f9a7",
|
||||
enabled: true,
|
||||
userId: 1,
|
||||
teamId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(Retell.verify).mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("should process web call with valid agent and charge credits", async () => {
|
||||
vi.mocked(PrismaAgentRepository.findByProviderAgentId).mockResolvedValue(mockAgent);
|
||||
mockChargeCredits.mockResolvedValue(undefined);
|
||||
|
||||
const body: RetellWebhookBody = {
|
||||
event: "call_analyzed",
|
||||
call: {
|
||||
call_id: "call_bcd94f5a50832873a5fd68cb1aa",
|
||||
call_type: "web_call",
|
||||
agent_id: "agent_5e3e0d29d692172c2c24d8f9a7",
|
||||
call_status: "ended",
|
||||
start_timestamp: 1757673314024,
|
||||
end_timestamp: 1757673321010,
|
||||
call_cost: {
|
||||
total_duration_seconds: 7,
|
||||
combined_cost: 1.3416667,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const request = createMockRequest(body, "valid-signature");
|
||||
const response = await callPOST(request);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const data = await response.json();
|
||||
expect(data.success).toBe(true);
|
||||
|
||||
expect(PrismaAgentRepository.findByProviderAgentId).toHaveBeenCalledWith({
|
||||
providerAgentId: "agent_5e3e0d29d692172c2c24d8f9a7",
|
||||
});
|
||||
|
||||
expect(mockChargeCredits).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
userId: 1,
|
||||
teamId: undefined,
|
||||
credits: 4, // 7 seconds = 0.117 minutes * $0.29 = $0.034 = 4 credits (rounded up)
|
||||
callDuration: 7,
|
||||
externalRef: "retell:call_bcd94f5a50832873a5fd68cb1aa",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle web call with team agent", async () => {
|
||||
const teamAgent = { ...mockAgent, userId: 2, teamId: 10 };
|
||||
vi.mocked(PrismaAgentRepository.findByProviderAgentId).mockResolvedValue(teamAgent);
|
||||
mockChargeCredits.mockResolvedValue(undefined);
|
||||
|
||||
const body: RetellWebhookBody = {
|
||||
event: "call_analyzed",
|
||||
call: {
|
||||
call_id: "web-call-team",
|
||||
call_type: "web_call",
|
||||
agent_id: "agent_team_123",
|
||||
call_status: "ended",
|
||||
start_timestamp: 1757673314024,
|
||||
call_cost: {
|
||||
total_duration_seconds: 60,
|
||||
combined_cost: 2.0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const request = createMockRequest(body, "valid-signature");
|
||||
const response = await callPOST(request);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockChargeCredits).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
userId: 2,
|
||||
teamId: 10,
|
||||
credits: 29, // 60 seconds = 1 minute * $0.29 = 29 credits
|
||||
callDuration: 60,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle web call without from_number", async () => {
|
||||
vi.mocked(PrismaAgentRepository.findByProviderAgentId).mockResolvedValue(mockAgent);
|
||||
mockChargeCredits.mockResolvedValue(undefined);
|
||||
|
||||
const body: RetellWebhookBody = {
|
||||
event: "call_analyzed",
|
||||
call: {
|
||||
call_id: "web-call-no-phone",
|
||||
agent_id: "agent_5e3e0d29d692172c2c24d8f9a7",
|
||||
call_status: "ended",
|
||||
start_timestamp: 1757673314024,
|
||||
call_cost: {
|
||||
total_duration_seconds: 30,
|
||||
combined_cost: 1.0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const request = createMockRequest(body, "valid-signature");
|
||||
const response = await callPOST(request);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(PrismaAgentRepository.findByProviderAgentId).toHaveBeenCalled();
|
||||
expect(PrismaPhoneNumberRepository.findByPhoneNumber).not.toHaveBeenCalled();
|
||||
expect(mockChargeCredits).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle web call with missing agent_id", async () => {
|
||||
const body: RetellWebhookBody = {
|
||||
event: "call_analyzed",
|
||||
call: {
|
||||
call_id: "web-call-no-agent",
|
||||
call_type: "web_call",
|
||||
call_status: "ended",
|
||||
call_cost: {
|
||||
total_duration_seconds: 30,
|
||||
combined_cost: 1.0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const request = createMockRequest(body, "valid-signature");
|
||||
const response = await callPOST(request);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(PrismaAgentRepository.findByProviderAgentId).not.toHaveBeenCalled();
|
||||
expect(mockChargeCredits).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle web call with agent not found", async () => {
|
||||
vi.mocked(PrismaAgentRepository.findByProviderAgentId).mockResolvedValue(null);
|
||||
|
||||
const body: RetellWebhookBody = {
|
||||
event: "call_analyzed",
|
||||
call: {
|
||||
call_id: "web-call-no-agent-found",
|
||||
call_type: "web_call",
|
||||
agent_id: "non-existent-agent",
|
||||
call_status: "ended",
|
||||
start_timestamp: 1757673314024,
|
||||
call_cost: {
|
||||
total_duration_seconds: 30,
|
||||
combined_cost: 1.0,
|
||||
},
|
||||
// Web calls don't have from_number, to_number, direction
|
||||
},
|
||||
};
|
||||
|
||||
const request = createMockRequest(body, "valid-signature");
|
||||
const response = await callPOST(request);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(PrismaAgentRepository.findByProviderAgentId).toHaveBeenCalledWith({
|
||||
providerAgentId: "non-existent-agent",
|
||||
});
|
||||
expect(mockChargeCredits).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { z } from "zod";
|
||||
import { CreditService } from "@calcom/features/ee/billing/credit-service";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { PrismaAgentRepository } from "@calcom/lib/server/repository/PrismaAgentRepository";
|
||||
import { PrismaPhoneNumberRepository } from "@calcom/lib/server/repository/PrismaPhoneNumberRepository";
|
||||
import { CreditUsageType } from "@calcom/prisma/enums";
|
||||
|
||||
@@ -18,9 +19,11 @@ const RetellWebhookSchema = z.object({
|
||||
.object({
|
||||
call_id: z.string(),
|
||||
agent_id: z.string().optional(),
|
||||
from_number: z.string(),
|
||||
to_number: z.string(),
|
||||
direction: z.enum(["inbound", "outbound"]),
|
||||
// Make phone fields optional for web calls
|
||||
from_number: z.string().optional(),
|
||||
to_number: z.string().optional(),
|
||||
direction: z.enum(["inbound", "outbound"]).optional(),
|
||||
call_type: z.string().optional(),
|
||||
call_status: z.string(),
|
||||
start_timestamp: z.number(),
|
||||
end_timestamp: z.number().optional(),
|
||||
@@ -59,44 +62,27 @@ const RetellWebhookSchema = z.object({
|
||||
.passthrough(),
|
||||
});
|
||||
|
||||
async function handleCallAnalyzed(callData: any) {
|
||||
const { from_number, call_id, call_cost } = callData;
|
||||
if (
|
||||
!call_cost ||
|
||||
typeof call_cost.total_duration_seconds !== "number" ||
|
||||
!Number.isFinite(call_cost.total_duration_seconds) ||
|
||||
call_cost.total_duration_seconds <= 0
|
||||
) {
|
||||
log.error(
|
||||
`Invalid or missing call_cost.total_duration_seconds for call ${call_id}: ${safeStringify(call_cost)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const phoneNumber = await PrismaPhoneNumberRepository.findByPhoneNumber({ phoneNumber: from_number });
|
||||
|
||||
if (!phoneNumber) {
|
||||
log.error(`No phone number found for ${from_number}, cannot deduct credits`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Support both personal and team phone numbers
|
||||
const userId = phoneNumber.userId;
|
||||
const teamId = phoneNumber.teamId;
|
||||
|
||||
if (!userId && !teamId) {
|
||||
log.error(`Phone number ${from_number} has no associated user or team`);
|
||||
return;
|
||||
}
|
||||
|
||||
async function chargeCreditsForCall({
|
||||
userId,
|
||||
teamId,
|
||||
callCost,
|
||||
callId,
|
||||
callDuration,
|
||||
}: {
|
||||
userId?: number;
|
||||
teamId?: number;
|
||||
callCost: number;
|
||||
callId: string;
|
||||
callDuration: number;
|
||||
}) {
|
||||
const rawRatePerMinute = process.env.CAL_AI_CALL_RATE_PER_MINUTE ?? "0.29";
|
||||
const ratePerMinute = Number.parseFloat(rawRatePerMinute);
|
||||
const safeRatePerMinute = Number.isFinite(ratePerMinute) && ratePerMinute > 0 ? ratePerMinute : 0.29;
|
||||
|
||||
const durationInMinutes = call_cost.total_duration_seconds / 60;
|
||||
const callCost = durationInMinutes * safeRatePerMinute;
|
||||
const durationInMinutes = callDuration / 60;
|
||||
const calculatedCallCost = durationInMinutes * safeRatePerMinute;
|
||||
// Convert to cents and round up to ensure we don't undercharge
|
||||
const creditsToDeduct = Math.ceil(callCost * 100);
|
||||
const creditsToDeduct = Math.ceil(calculatedCallCost * 100);
|
||||
|
||||
const creditService = new CreditService();
|
||||
|
||||
@@ -105,15 +91,22 @@ async function handleCallAnalyzed(callData: any) {
|
||||
userId: userId ?? undefined,
|
||||
teamId: teamId ?? undefined,
|
||||
credits: creditsToDeduct,
|
||||
callDuration: call_cost.total_duration_seconds,
|
||||
callDuration: callDuration,
|
||||
creditFor: CreditUsageType.CAL_AI_PHONE_CALL,
|
||||
externalRef: `retell:${call_id}`,
|
||||
externalRef: `retell:${callId}`,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully charged ${creditsToDeduct} credits (${callDuration}s at $${safeRatePerMinute}/min) for ${
|
||||
teamId ? `team:${teamId}` : ""
|
||||
} ${userId ? `user:${userId}` : ""}, call ${callId}`,
|
||||
};
|
||||
} catch (e) {
|
||||
log.error("Error charging credits for Retell AI call", {
|
||||
error: e,
|
||||
call_id,
|
||||
call_cost,
|
||||
call_id: callId,
|
||||
call_cost: callCost,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
@@ -124,15 +117,88 @@ async function handleCallAnalyzed(callData: any) {
|
||||
}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully charged ${creditsToDeduct} credits (${
|
||||
call_cost.total_duration_seconds
|
||||
}s at $${safeRatePerMinute}/min) for ${teamId ? `team:${teamId}` : ""} ${
|
||||
userId ? `user:${userId}` : ""
|
||||
}, call ${call_id}`,
|
||||
};
|
||||
async function handleCallAnalyzed(callData: any) {
|
||||
const { from_number, call_id, call_cost, call_type, agent_id } = callData;
|
||||
|
||||
if (
|
||||
!call_cost ||
|
||||
typeof call_cost.total_duration_seconds !== "number" ||
|
||||
!Number.isFinite(call_cost.total_duration_seconds) ||
|
||||
call_cost.total_duration_seconds <= 0
|
||||
) {
|
||||
log.info(
|
||||
`Invalid or missing call_cost.total_duration_seconds for call ${call_id}: ${safeStringify(call_cost)}`
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
message: `Invalid or missing call_cost.total_duration_seconds for call ${call_id}`
|
||||
};
|
||||
}
|
||||
|
||||
let userId: number | undefined;
|
||||
let teamId: number | undefined;
|
||||
|
||||
// Handle web calls vs phone calls
|
||||
if (call_type === "web_call" || !from_number) {
|
||||
if (!agent_id) {
|
||||
log.error(`Web call ${call_id} missing agent_id, cannot charge credits`);
|
||||
return {
|
||||
success: false,
|
||||
message: `Web call ${call_id} missing agent_id, cannot charge credits`
|
||||
};
|
||||
}
|
||||
|
||||
const agent = await PrismaAgentRepository.findByProviderAgentId({
|
||||
providerAgentId: agent_id,
|
||||
});
|
||||
|
||||
if (!agent) {
|
||||
log.error(`No agent found for providerAgentId ${agent_id}, call ${call_id}`);
|
||||
return {
|
||||
success: false,
|
||||
message: `No agent found for providerAgentId ${agent_id}, call ${call_id}`
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
userId = agent.userId ?? undefined;
|
||||
teamId = agent.teamId ?? undefined;
|
||||
|
||||
log.info(`Processing web call ${call_id} for agent ${agent_id}, user ${userId}, team ${teamId}`);
|
||||
} else {
|
||||
const phoneNumber = await PrismaPhoneNumberRepository.findByPhoneNumber({
|
||||
phoneNumber: from_number,
|
||||
});
|
||||
|
||||
if (!phoneNumber) {
|
||||
const msg = `No phone number found for ${from_number}, call ${call_id}`;
|
||||
log.error(msg);
|
||||
return { success: false, message: msg };
|
||||
}
|
||||
|
||||
userId = phoneNumber.userId ?? undefined;
|
||||
teamId = phoneNumber.teamId ?? undefined;
|
||||
|
||||
log.info(`Processing phone call ${call_id} from ${from_number}, user ${userId}, team ${teamId}`);
|
||||
}
|
||||
|
||||
if (!userId && !teamId) {
|
||||
log.error(`Call ${call_id} has no associated user or team`);
|
||||
return {
|
||||
success: false,
|
||||
message: `Call ${call_id} has no associated user or team`,
|
||||
};
|
||||
}
|
||||
|
||||
return await chargeCreditsForCall({
|
||||
userId,
|
||||
teamId,
|
||||
callCost: call_cost.combined_cost || 0,
|
||||
callId: call_id,
|
||||
callDuration: call_cost.total_duration_seconds,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -192,6 +258,8 @@ async function handler(request: NextRequest) {
|
||||
try {
|
||||
const payload = RetellWebhookSchema.parse(body);
|
||||
const callData = payload.call;
|
||||
|
||||
// Skip inbound calls (only for phone calls, web calls don't have direction)
|
||||
if (callData.direction === "inbound") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -202,7 +270,9 @@ async function handler(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
log.info(`Received Retell AI webhook: ${payload.event} for call ${callData.call_id}`);
|
||||
log.info(`Received Retell AI webhook: ${payload.event} for call ${callData.call_id}`, {
|
||||
call_id: callData.call_id,
|
||||
});
|
||||
|
||||
const result = await handleCallAnalyzed(callData);
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { DynamicComponent } from "@calcom/app-store/_components/DynamicComponent";
|
||||
|
||||
export const AppSetupMap = {
|
||||
alby: dynamic(() => import("@calcom/web/components/apps/alby/Setup")),
|
||||
"apple-calendar": dynamic(() => import("@calcom/web/components/apps/applecalendar/Setup")),
|
||||
exchange: dynamic(() => import("@calcom/web/components/apps/exchangecalendar/Setup")),
|
||||
"exchange2013-calendar": dynamic(() => import("@calcom/web/components/apps/exchange2013calendar/Setup")),
|
||||
"exchange2016-calendar": dynamic(() => import("@calcom/web/components/apps/exchange2016calendar/Setup")),
|
||||
"caldav-calendar": dynamic(() => import("@calcom/web/components/apps/caldavcalendar/Setup")),
|
||||
"ics-feed": dynamic(() => import("@calcom/web/components/apps/ics-feedcalendar/Setup")),
|
||||
zapier: dynamic(() => import("@calcom/web/components/apps/zapier/Setup")),
|
||||
make: dynamic(() => import("@calcom/web/components/apps/make/Setup")),
|
||||
sendgrid: dynamic(() => import("@calcom/web/components/apps/sendgrid/Setup")),
|
||||
stripe: dynamic(() => import("@calcom/web/components/apps/stripepayment/Setup")),
|
||||
paypal: dynamic(() => import("@calcom/web/components/apps/paypal/Setup")),
|
||||
hitpay: dynamic(() => import("@calcom/web/components/apps/hitpay/Setup")),
|
||||
btcpayserver: dynamic(() => import("@calcom/web/components/apps/btcpayserver/Setup")),
|
||||
};
|
||||
|
||||
export const AppSetupPage = (props: { slug: string }) => {
|
||||
return <DynamicComponent<typeof AppSetupMap> componentMap={AppSetupMap} {...props} />;
|
||||
};
|
||||
|
||||
export default AppSetupPage;
|
||||
+3
-10
@@ -5,22 +5,15 @@ import { useState, useCallback, useEffect } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
import AppNotInstalledMessage from "@calcom/app-store/_components/AppNotInstalledMessage";
|
||||
import { albyCredentialKeysSchema } from "@calcom/app-store/alby/lib/albyCredentialKeysSchema";
|
||||
import type { IAlbySetupProps } from "@calcom/app-store/alby/pages/setup/_getServerSideProps";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
|
||||
import { albyCredentialKeysSchema } from "../../lib/albyCredentialKeysSchema";
|
||||
|
||||
export interface IAlbySetupProps {
|
||||
email: string | null;
|
||||
lightningAddress: string | null;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
}
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
export default function AlbySetup(props: IAlbySetupProps) {
|
||||
const params = useCompatSearchParams();
|
||||
+2
-2
@@ -5,10 +5,10 @@ import { Toaster } from "sonner";
|
||||
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { PasswordField } from "@calcom/ui/components/form";
|
||||
import { TextField } from "@calcom/ui/components/form";
|
||||
import { Alert } from "@calcom/ui/components/alert";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { PasswordField } from "@calcom/ui/components/form";
|
||||
import { TextField } from "@calcom/ui/components/form";
|
||||
import { Form } from "@calcom/ui/components/form";
|
||||
|
||||
export default function AppleCalendarSetup() {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user