Compare commits
60
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4599740637 | ||
|
|
8d17ca536e | ||
|
|
3fa7b30485 | ||
|
|
d9adb6c1fc | ||
|
|
f4944a05c9 | ||
|
|
6ba49a73df | ||
|
|
babeb1f31d | ||
|
|
13cbb59e5e | ||
|
|
8d2a072645 | ||
|
|
b2244503b1 | ||
|
|
9d67e1c473 | ||
|
|
489d56dad1 | ||
|
|
532c2f57a1 | ||
|
|
efca90507b | ||
|
|
28ff809739 | ||
|
|
8e73421094 | ||
|
|
5bab2af500 | ||
|
|
9b7c6585d8 | ||
|
|
58930c4c5e | ||
|
|
9412226a76 | ||
|
|
df12581879 | ||
|
|
0ec1638ca7 | ||
|
|
2516e18ebb | ||
|
|
d2f8076c70 | ||
|
|
cbfb11015e | ||
|
|
2d90d11e9e | ||
|
|
7ae0d6b7cc | ||
|
|
e0dc2ba03a | ||
|
|
aea6e105e6 | ||
|
|
a082cf73a0 | ||
|
|
61268f36b9 | ||
|
|
99cd0c5c41 | ||
|
|
2266301469 | ||
|
|
30b0012e6a | ||
|
|
77d1958c42 | ||
|
|
88f347f18c | ||
|
|
09cf8885cf | ||
|
|
f3d4eab951 | ||
|
|
866b6cc233 | ||
|
|
cafecad8ae | ||
|
|
0ea673e507 | ||
|
|
cf367c9340 | ||
|
|
d72e15649f | ||
|
|
89a3b77a1d | ||
|
|
38f6f71ae0 | ||
|
|
40caa82e55 | ||
|
|
b9aee3b18c | ||
|
|
3f8d2f2b38 | ||
|
|
c3731d9f17 | ||
|
|
6e4b2d8886 | ||
|
|
cf13c06c57 | ||
|
|
e309e72be3 | ||
|
|
b4d804de7c | ||
|
|
f501ef3269 | ||
|
|
79f1074f93 | ||
|
|
48aa6bf489 | ||
|
|
8ed2aacd9e | ||
|
|
dfc5d914fc | ||
|
|
0a11c45065 | ||
|
|
4ae5de5a3a |
@@ -0,0 +1,187 @@
|
|||||||
|
language: en-US
|
||||||
|
tone_instructions: ''
|
||||||
|
early_access: false
|
||||||
|
enable_free_tier: true
|
||||||
|
reviews:
|
||||||
|
profile: chill
|
||||||
|
request_changes_workflow: false
|
||||||
|
high_level_summary: false
|
||||||
|
high_level_summary_placeholder: '@coderabbitai summary'
|
||||||
|
high_level_summary_in_walkthrough: false
|
||||||
|
auto_title_placeholder: '@coderabbitai'
|
||||||
|
auto_title_instructions: ''
|
||||||
|
review_status: true
|
||||||
|
commit_status: true
|
||||||
|
fail_commit_status: false
|
||||||
|
collapse_walkthrough: false
|
||||||
|
changed_files_summary: false
|
||||||
|
sequence_diagrams: false
|
||||||
|
estimate_code_review_effort: false
|
||||||
|
assess_linked_issues: true
|
||||||
|
related_issues: false
|
||||||
|
related_prs: true
|
||||||
|
suggested_labels: false
|
||||||
|
auto_apply_labels: false
|
||||||
|
suggested_reviewers: false
|
||||||
|
auto_assign_reviewers: false
|
||||||
|
poem: false
|
||||||
|
labeling_instructions: []
|
||||||
|
path_filters:
|
||||||
|
- '!**/node_modules/**'
|
||||||
|
- '!**/dist/**'
|
||||||
|
- '!**/build/**'
|
||||||
|
- '!**/.next/**'
|
||||||
|
path_instructions:
|
||||||
|
- path: "**/*.{ts,tsx,js,jsx}"
|
||||||
|
instructions: "Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module."
|
||||||
|
abort_on_close: true
|
||||||
|
disable_cache: false
|
||||||
|
auto_review:
|
||||||
|
enabled: true
|
||||||
|
auto_incremental_review: true
|
||||||
|
ignore_title_keywords: []
|
||||||
|
labels: []
|
||||||
|
drafts: false
|
||||||
|
base_branches: []
|
||||||
|
finishing_touches:
|
||||||
|
docstrings:
|
||||||
|
enabled: true
|
||||||
|
unit_tests:
|
||||||
|
enabled: true
|
||||||
|
pre_merge_checks:
|
||||||
|
docstrings:
|
||||||
|
mode: warning
|
||||||
|
threshold: 80
|
||||||
|
title:
|
||||||
|
mode: warning
|
||||||
|
requirements: ''
|
||||||
|
description:
|
||||||
|
mode: warning
|
||||||
|
issue_assessment:
|
||||||
|
mode: warning
|
||||||
|
tools:
|
||||||
|
ast-grep:
|
||||||
|
rule_dirs: []
|
||||||
|
util_dirs: []
|
||||||
|
essential_rules: true
|
||||||
|
packages: []
|
||||||
|
shellcheck:
|
||||||
|
enabled: true
|
||||||
|
ruff:
|
||||||
|
enabled: true
|
||||||
|
markdownlint:
|
||||||
|
enabled: true
|
||||||
|
github-checks:
|
||||||
|
enabled: true
|
||||||
|
timeout_ms: 180000
|
||||||
|
languagetool:
|
||||||
|
enabled: true
|
||||||
|
enabled_rules: []
|
||||||
|
disabled_rules: []
|
||||||
|
enabled_categories: []
|
||||||
|
disabled_categories: []
|
||||||
|
enabled_only: false
|
||||||
|
level: default
|
||||||
|
biome:
|
||||||
|
enabled: true
|
||||||
|
hadolint:
|
||||||
|
enabled: true
|
||||||
|
swiftlint:
|
||||||
|
enabled: true
|
||||||
|
phpstan:
|
||||||
|
enabled: true
|
||||||
|
level: default
|
||||||
|
phpmd:
|
||||||
|
enabled: true
|
||||||
|
phpcs:
|
||||||
|
enabled: true
|
||||||
|
golangci-lint:
|
||||||
|
enabled: true
|
||||||
|
yamllint:
|
||||||
|
enabled: true
|
||||||
|
gitleaks:
|
||||||
|
enabled: true
|
||||||
|
checkov:
|
||||||
|
enabled: true
|
||||||
|
detekt:
|
||||||
|
enabled: true
|
||||||
|
eslint:
|
||||||
|
enabled: true
|
||||||
|
flake8:
|
||||||
|
enabled: true
|
||||||
|
rubocop:
|
||||||
|
enabled: true
|
||||||
|
buf:
|
||||||
|
enabled: true
|
||||||
|
regal:
|
||||||
|
enabled: true
|
||||||
|
actionlint:
|
||||||
|
enabled: true
|
||||||
|
pmd:
|
||||||
|
enabled: true
|
||||||
|
cppcheck:
|
||||||
|
enabled: true
|
||||||
|
semgrep:
|
||||||
|
enabled: true
|
||||||
|
circleci:
|
||||||
|
enabled: true
|
||||||
|
clippy:
|
||||||
|
enabled: true
|
||||||
|
sqlfluff:
|
||||||
|
enabled: true
|
||||||
|
prismaLint:
|
||||||
|
enabled: true
|
||||||
|
pylint:
|
||||||
|
enabled: true
|
||||||
|
oxc:
|
||||||
|
enabled: true
|
||||||
|
shopifyThemeCheck:
|
||||||
|
enabled: true
|
||||||
|
luacheck:
|
||||||
|
enabled: true
|
||||||
|
brakeman:
|
||||||
|
enabled: true
|
||||||
|
dotenvLint:
|
||||||
|
enabled: true
|
||||||
|
htmlhint:
|
||||||
|
enabled: true
|
||||||
|
checkmake:
|
||||||
|
enabled: true
|
||||||
|
osvScanner:
|
||||||
|
enabled: true
|
||||||
|
chat:
|
||||||
|
art: true
|
||||||
|
auto_reply: true
|
||||||
|
integrations:
|
||||||
|
jira:
|
||||||
|
usage: auto
|
||||||
|
linear:
|
||||||
|
usage: auto
|
||||||
|
knowledge_base:
|
||||||
|
opt_out: false
|
||||||
|
web_search:
|
||||||
|
enabled: true
|
||||||
|
code_guidelines:
|
||||||
|
enabled: true
|
||||||
|
filePatterns: []
|
||||||
|
learnings:
|
||||||
|
scope: auto
|
||||||
|
issues:
|
||||||
|
scope: auto
|
||||||
|
jira:
|
||||||
|
usage: auto
|
||||||
|
project_keys: []
|
||||||
|
linear:
|
||||||
|
usage: auto
|
||||||
|
team_keys: []
|
||||||
|
pull_requests:
|
||||||
|
scope: auto
|
||||||
|
mcp:
|
||||||
|
usage: auto
|
||||||
|
disabled_servers: []
|
||||||
|
code_generation:
|
||||||
|
docstrings:
|
||||||
|
language: en-US
|
||||||
|
path_instructions: []
|
||||||
|
unit_tests:
|
||||||
|
path_instructions: []
|
||||||
@@ -74,8 +74,14 @@ jobs:
|
|||||||
uses: ./.github/workflows/e2e-embed-react.yml
|
uses: ./.github/workflows/e2e-embed-react.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e-atoms:
|
||||||
|
name: Tests
|
||||||
|
needs: [lint, build, build-atoms, build-api-v2]
|
||||||
|
uses: ./.github/workflows/e2e-atoms.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
required:
|
required:
|
||||||
needs: [lint, type-check, unit-test, integration-test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
needs: [lint, type-check, unit-test, integration-test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store, e2e-atoms]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
name: Atoms E2E Tests
|
name: Atoms E2E Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
workflow_call:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- 'packages/platform/atoms/**'
|
|
||||||
- 'packages/platform/examples/base/**'
|
|
||||||
- 'apps/api/v2/**'
|
|
||||||
- '.github/workflows/atoms-e2e.yml'
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
actions: write
|
actions: write
|
||||||
contents: read
|
contents: read
|
||||||
@@ -25,7 +17,7 @@ env:
|
|||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
atoms-e2e:
|
e2e-atoms:
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
name: Atoms E2E Tests
|
name: Atoms E2E Tests
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
@@ -45,13 +37,13 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: atoms-e2e-test-results
|
name: e2e-atoms-test-results
|
||||||
path: test-results
|
path: test-results
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: Upload Playwright Report
|
- name: Upload Playwright Report
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: atoms-e2e-playwright-report
|
name: e2e-atoms-playwright-report
|
||||||
path: playwright-report
|
path: playwright-report
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
@@ -195,6 +195,13 @@ jobs:
|
|||||||
uses: ./.github/workflows/e2e-embed-react.yml
|
uses: ./.github/workflows/e2e-embed-react.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e-atoms:
|
||||||
|
name: Tests
|
||||||
|
needs: [changes, check-label, build, build-atoms, build-api-v2]
|
||||||
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
|
uses: ./.github/workflows/e2e-atoms.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze Build
|
name: Analyze Build
|
||||||
needs: [build]
|
needs: [build]
|
||||||
@@ -204,7 +211,7 @@ jobs:
|
|||||||
merge-reports:
|
merge-reports:
|
||||||
name: Merge reports
|
name: Merge reports
|
||||||
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
needs: [changes, check-label, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
needs: [changes, check-label, e2e, e2e-embed, e2e-embed-react, e2e-app-store, e2e-atoms]
|
||||||
uses: ./.github/workflows/merge-reports.yml
|
uses: ./.github/workflows/merge-reports.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
@@ -249,6 +256,7 @@ jobs:
|
|||||||
e2e-embed,
|
e2e-embed,
|
||||||
e2e-embed-react,
|
e2e-embed-react,
|
||||||
e2e-app-store,
|
e2e-app-store,
|
||||||
|
e2e-atoms,
|
||||||
]
|
]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
|
|||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/prisma"
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"functions": {
|
|
||||||
"pages/api/slots/*.ts": {
|
|
||||||
"memory": 512
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"@axiomhq/winston": "^1.2.0",
|
"@axiomhq/winston": "^1.2.0",
|
||||||
"@calcom/platform-constants": "*",
|
"@calcom/platform-constants": "*",
|
||||||
"@calcom/platform-enums": "*",
|
"@calcom/platform-enums": "*",
|
||||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.318",
|
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.321",
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "*",
|
||||||
"@calcom/platform-utils": "*",
|
"@calcom/platform-utils": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { TestingModule } from "@nestjs/testing";
|
|||||||
import { Test } from "@nestjs/testing";
|
import { Test } from "@nestjs/testing";
|
||||||
import * as request from "supertest";
|
import * as request from "supertest";
|
||||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||||
|
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||||
@@ -33,6 +34,7 @@ describe("AppController", () => {
|
|||||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||||
|
let membershipRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
|
||||||
let apiKeyString: string;
|
let apiKeyString: string;
|
||||||
|
|
||||||
@@ -108,6 +110,14 @@ describe("AppController", () => {
|
|||||||
organization: { connect: { id: organization.id } },
|
organization: { connect: { id: organization.id } },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
await membershipRepositoryFixture.create({
|
||||||
|
user: { connect: { id: user.id } },
|
||||||
|
team: { connect: { id: organization.id } },
|
||||||
|
role: "OWNER",
|
||||||
|
accepted: true,
|
||||||
|
});
|
||||||
|
|
||||||
app = moduleRef.createNestApplication();
|
app = moduleRef.createNestApplication();
|
||||||
await app.init();
|
await app.init();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
|
|||||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
import { KyselyModule } from "@/modules/kysely/kysely.module";
|
import { KyselyModule } from "@/modules/kysely/kysely.module";
|
||||||
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
@@ -53,6 +54,7 @@ import { Module } from "@nestjs/common";
|
|||||||
StripeModule,
|
StripeModule,
|
||||||
TeamsModule,
|
TeamsModule,
|
||||||
TeamsEventTypesModule,
|
TeamsEventTypesModule,
|
||||||
|
MembershipsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
TokensRepository,
|
TokensRepository,
|
||||||
|
|||||||
@@ -13,10 +13,14 @@ import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.ser
|
|||||||
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
|
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
|
||||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||||
|
import {
|
||||||
|
AuthOptionalUser,
|
||||||
|
GetOptionalUser,
|
||||||
|
} from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
|
||||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { UsersService } from "@/modules/users/services/users.service";
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
@@ -96,6 +100,7 @@ export class BookingsController_2024_08_13 {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
|
@UseGuards(OptionalApiAuthGuard)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Create a booking",
|
summary: "Create a booking",
|
||||||
description: `
|
description: `
|
||||||
@@ -138,9 +143,10 @@ export class BookingsController_2024_08_13 {
|
|||||||
async createBooking(
|
async createBooking(
|
||||||
@Body(new CreateBookingInputPipe())
|
@Body(new CreateBookingInputPipe())
|
||||||
body: CreateBookingInput,
|
body: CreateBookingInput,
|
||||||
@Req() request: Request
|
@Req() request: Request,
|
||||||
|
@GetOptionalUser() user: AuthOptionalUser
|
||||||
): Promise<CreateBookingOutput_2024_08_13> {
|
): Promise<CreateBookingOutput_2024_08_13> {
|
||||||
const booking = await this.bookingsService.createBooking(request, body);
|
const booking = await this.bookingsService.createBooking(request, body, user);
|
||||||
|
|
||||||
if (Array.isArray(booking)) {
|
if (Array.isArray(booking)) {
|
||||||
await this.bookingsService.billBookings(booking);
|
await this.bookingsService.billBookings(booking);
|
||||||
|
|||||||
+73
-1
@@ -3,6 +3,7 @@ import { AppModule } from "@/app.module";
|
|||||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||||
import { Locales } from "@/lib/enums/locales";
|
import { Locales } from "@/lib/enums/locales";
|
||||||
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
import {
|
import {
|
||||||
CreateManagedUserData,
|
CreateManagedUserData,
|
||||||
CreateManagedUserOutput,
|
CreateManagedUserOutput,
|
||||||
@@ -80,7 +81,7 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await Test.createTestingModule({
|
const moduleRef = await Test.createTestingModule({
|
||||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||||
imports: [AppModule, UsersModule],
|
imports: [AppModule, UsersModule, MembershipsModule],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
app = moduleRef.createNestApplication();
|
app = moduleRef.createNestApplication();
|
||||||
@@ -772,6 +773,77 @@ describe("Managed user bookings 2024-08-13", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("event type booking requires authentication", () => {
|
||||||
|
let eventTypeRequiringAuthenticationId: number;
|
||||||
|
|
||||||
|
let body: CreateBookingInput_2024_08_13;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const eventTypeRequiringAuthentication = await eventTypesRepositoryFixture.create(
|
||||||
|
{
|
||||||
|
title: `event-type-requiring-authentication-${randomString()}`,
|
||||||
|
slug: `event-type-requiring-authentication-${randomString()}`,
|
||||||
|
length: 60,
|
||||||
|
requiresConfirmation: true,
|
||||||
|
bookingRequiresAuthentication: true,
|
||||||
|
},
|
||||||
|
secondManagedUser.user.id
|
||||||
|
);
|
||||||
|
eventTypeRequiringAuthenticationId = eventTypeRequiringAuthentication.id;
|
||||||
|
|
||||||
|
body = {
|
||||||
|
start: new Date(Date.UTC(2030, 0, 9, 15, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: eventTypeRequiringAuthenticationId,
|
||||||
|
attendee: {
|
||||||
|
email: "external@example.com",
|
||||||
|
name: "External Attendee",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can't be booked without credentials", async () => {
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings`)
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can't be booked with managed user credentials who is not admin and not event type owner", async () => {
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings`)
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
|
||||||
|
.expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can be booked with managed user credentials who is event type owner", async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings`)
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const bookingId = response.body.data.id;
|
||||||
|
await bookingsRepositoryFixture.deleteById(bookingId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can be booked with managed user credentials who is admin", async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings`)
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const bookingId = response.body.data.id;
|
||||||
|
await bookingsRepositoryFixture.deleteById(bookingId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await userRepositoryFixture.delete(firstManagedUser.user.id);
|
await userRepositoryFixture.delete(firstManagedUser.user.id);
|
||||||
await userRepositoryFixture.delete(secondManagedUser.user.id);
|
await userRepositoryFixture.delete(secondManagedUser.user.id);
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repo
|
|||||||
import { BookingSeatRepositoryFixture } from "test/fixtures/repository/booking-seat.repository.fixture";
|
import { BookingSeatRepositoryFixture } from "test/fixtures/repository/booking-seat.repository.fixture";
|
||||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
|
||||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { randomString } from "test/utils/randomString";
|
import { randomString } from "test/utils/randomString";
|
||||||
@@ -27,7 +26,6 @@ import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@cal
|
|||||||
import {
|
import {
|
||||||
CancelBookingInput_2024_08_13,
|
CancelBookingInput_2024_08_13,
|
||||||
CancelSeatedBookingInput_2024_08_13,
|
CancelSeatedBookingInput_2024_08_13,
|
||||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
|
||||||
CreateSeatedBookingOutput_2024_08_13,
|
CreateSeatedBookingOutput_2024_08_13,
|
||||||
GetBookingOutput_2024_08_13,
|
GetBookingOutput_2024_08_13,
|
||||||
GetBookingsOutput_2024_08_13,
|
GetBookingsOutput_2024_08_13,
|
||||||
@@ -35,11 +33,9 @@ import {
|
|||||||
RescheduleSeatedBookingInput_2024_08_13,
|
RescheduleSeatedBookingInput_2024_08_13,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
import { CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
|
import { CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
|
||||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
import { Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
describe("Bookings Endpoints 2024-08-13", () => {
|
describe("Bookings Endpoints 2024-08-13", () => {
|
||||||
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
|
|
||||||
|
|
||||||
describe("Seated bookings", () => {
|
describe("Seated bookings", () => {
|
||||||
let app: INestApplication;
|
let app: INestApplication;
|
||||||
let organization: Team;
|
let organization: Team;
|
||||||
@@ -48,7 +44,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||||
let schedulesService: SchedulesService_2024_04_15;
|
let schedulesService: SchedulesService_2024_04_15;
|
||||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
|
||||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||||
let bookingSeatRepositoryFixture: BookingSeatRepositoryFixture;
|
let bookingSeatRepositoryFixture: BookingSeatRepositoryFixture;
|
||||||
@@ -58,7 +53,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
let apiKeyString: string;
|
let apiKeyString: string;
|
||||||
|
|
||||||
let seatedEventTypeId: number;
|
let seatedEventTypeId: number;
|
||||||
const maxRecurrenceCount = 3;
|
|
||||||
|
|
||||||
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
|
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
|
||||||
|
|
||||||
@@ -83,7 +77,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
|
||||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||||
@@ -190,6 +183,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
absent: false,
|
absent: false,
|
||||||
seatUid,
|
seatUid,
|
||||||
bookingFieldsResponses: {
|
bookingFieldsResponses: {
|
||||||
|
guests: [],
|
||||||
name: body.attendee.name,
|
name: body.attendee.name,
|
||||||
...body.bookingFieldsResponses,
|
...body.bookingFieldsResponses,
|
||||||
},
|
},
|
||||||
@@ -264,6 +258,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
absent: false,
|
absent: false,
|
||||||
seatUid: createdSeatedBooking.seatUid,
|
seatUid: createdSeatedBooking.seatUid,
|
||||||
bookingFieldsResponses: {
|
bookingFieldsResponses: {
|
||||||
|
guests: [],
|
||||||
name: createdSeatedBooking.attendees[0].name,
|
name: createdSeatedBooking.attendees[0].name,
|
||||||
...createdSeatedBooking.attendees[0].bookingFieldsResponses,
|
...createdSeatedBooking.attendees[0].bookingFieldsResponses,
|
||||||
},
|
},
|
||||||
@@ -278,6 +273,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
absent: false,
|
absent: false,
|
||||||
seatUid,
|
seatUid,
|
||||||
bookingFieldsResponses: {
|
bookingFieldsResponses: {
|
||||||
|
guests: [],
|
||||||
name: body.attendee.name,
|
name: body.attendee.name,
|
||||||
...body.bookingFieldsResponses,
|
...body.bookingFieldsResponses,
|
||||||
},
|
},
|
||||||
@@ -517,6 +513,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
absent: false,
|
absent: false,
|
||||||
seatUid,
|
seatUid,
|
||||||
bookingFieldsResponses: {
|
bookingFieldsResponses: {
|
||||||
|
guests: [],
|
||||||
name: body.attendee.name,
|
name: body.attendee.name,
|
||||||
...body.bookingFieldsResponses,
|
...body.bookingFieldsResponses,
|
||||||
},
|
},
|
||||||
|
|||||||
+76
-1
@@ -21,7 +21,12 @@ import { randomString } from "test/utils/randomString";
|
|||||||
import { withApiAuth } from "test/utils/withApiAuth";
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||||
import { CreateBookingInput_2024_08_13, BookingOutput_2024_08_13 } from "@calcom/platform-types";
|
import {
|
||||||
|
CreateBookingInput_2024_08_13,
|
||||||
|
BookingOutput_2024_08_13,
|
||||||
|
CreateRecurringBookingInput_2024_08_13,
|
||||||
|
RecurringBookingOutput_2024_08_13,
|
||||||
|
} from "@calcom/platform-types";
|
||||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
describe("Bookings Endpoints 2024-08-13", () => {
|
describe("Bookings Endpoints 2024-08-13", () => {
|
||||||
@@ -44,6 +49,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
const VARIABLE_LENGTH_EVENT_TYPE_SLUG = `variable-length-bookings-2024-08-13-event-type-${randomString()}`;
|
const VARIABLE_LENGTH_EVENT_TYPE_SLUG = `variable-length-bookings-2024-08-13-event-type-${randomString()}`;
|
||||||
let normalEventType: EventType;
|
let normalEventType: EventType;
|
||||||
const NORMAL_EVENT_TYPE_SLUG = `variable-length-bookings-2024-08-13-event-type-${randomString()}`;
|
const NORMAL_EVENT_TYPE_SLUG = `variable-length-bookings-2024-08-13-event-type-${randomString()}`;
|
||||||
|
let variableLengthRecurringEventType: EventType;
|
||||||
|
const VARIABLE_LENGTH_RECURRING_EVENT_TYPE_SLUG = `variable-length-recurring-bookings-2024-08-13-event-type-${randomString()}`;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withApiAuth(
|
const moduleRef = await withApiAuth(
|
||||||
@@ -105,6 +112,17 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
user.id
|
user.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
variableLengthRecurringEventType = await eventTypesRepositoryFixture.create(
|
||||||
|
{
|
||||||
|
title: `variable-length-recurring-bookings-2024-08-13-event-type-${randomString()}`,
|
||||||
|
slug: VARIABLE_LENGTH_RECURRING_EVENT_TYPE_SLUG,
|
||||||
|
length: 15,
|
||||||
|
metadata: { multipleDuration: [15, 30, 60] },
|
||||||
|
recurringEvent: { freq: 2, count: 3, interval: 1 }, // Weekly, 3 times, every 1 week
|
||||||
|
},
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
|
||||||
app = moduleRef.createNestApplication();
|
app = moduleRef.createNestApplication();
|
||||||
bootstrap(app as NestExpressApplication);
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
@@ -246,6 +264,63 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("create recurring bookings", () => {
|
||||||
|
it("should create recurring bookings with custom lengthInMinutes", async () => {
|
||||||
|
const lengthInMinutes = 30;
|
||||||
|
const body: CreateRecurringBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2030, 1, 1, 14, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: variableLengthRecurringEventType.id,
|
||||||
|
lengthInMinutes,
|
||||||
|
recurrenceCount: 2,
|
||||||
|
attendee: {
|
||||||
|
name: "Mr Recurring",
|
||||||
|
email: "mr_recurring@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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: { status: string; data: RecurringBookingOutput_2024_08_13[] } = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data).toBeDefined();
|
||||||
|
expect(Array.isArray(responseBody.data)).toBe(true);
|
||||||
|
expect(responseBody.data).toHaveLength(2);
|
||||||
|
|
||||||
|
const firstBooking = responseBody.data[0];
|
||||||
|
const secondBooking = responseBody.data[1];
|
||||||
|
expect(firstBooking.duration).toEqual(lengthInMinutes);
|
||||||
|
expect(secondBooking.duration).toEqual(lengthInMinutes);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject recurring booking with invalid lengthInMinutes", async () => {
|
||||||
|
const body: CreateRecurringBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2030, 3, 1, 10, 0, 0)).toISOString(),
|
||||||
|
eventTypeId: variableLengthRecurringEventType.id,
|
||||||
|
lengthInMinutes: 45, // Not in allowed [15, 30, 60]
|
||||||
|
recurrenceCount: 2,
|
||||||
|
attendee: {
|
||||||
|
name: "Mr Invalid",
|
||||||
|
email: "mr_invalid@gmail.com",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
language: "it",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post("/v2/bookings")
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||||
await teamRepositoryFixture.delete(organization.id);
|
await teamRepositoryFixture.delete(organization.id);
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/servi
|
|||||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||||
import { getPagination } from "@/lib/pagination/pagination";
|
import { getPagination } from "@/lib/pagination/pagination";
|
||||||
|
import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
|
||||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||||
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
|
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
|
||||||
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
|
import { MembershipsService } from "@/modules/memberships/services/memberships.service";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||||
@@ -18,7 +21,14 @@ import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-eve
|
|||||||
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||||
import { UsersService } from "@/modules/users/services/users.service";
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
import { UsersRepository, UserWithProfile } from "@/modules/users/users.repository";
|
import { UsersRepository, UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import { ConflictException, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
import {
|
||||||
|
ConflictException,
|
||||||
|
ForbiddenException,
|
||||||
|
Injectable,
|
||||||
|
Logger,
|
||||||
|
NotFoundException,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from "@nestjs/common";
|
||||||
import { BadRequestException } from "@nestjs/common";
|
import { BadRequestException } from "@nestjs/common";
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
@@ -95,10 +105,12 @@ export class BookingsService_2024_08_13 {
|
|||||||
private readonly organizationsRepository: OrganizationsRepository,
|
private readonly organizationsRepository: OrganizationsRepository,
|
||||||
private readonly teamsRepository: TeamsRepository,
|
private readonly teamsRepository: TeamsRepository,
|
||||||
private readonly teamsEventTypesRepository: TeamsEventTypesRepository,
|
private readonly teamsEventTypesRepository: TeamsEventTypesRepository,
|
||||||
|
private readonly membershipsRepository: MembershipsRepository,
|
||||||
|
private readonly membershipsService: MembershipsService,
|
||||||
private readonly errorsBookingsService: ErrorsBookingsService_2024_08_13
|
private readonly errorsBookingsService: ErrorsBookingsService_2024_08_13
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createBooking(request: Request, body: CreateBookingInput) {
|
async createBooking(request: Request, body: CreateBookingInput, authUser: AuthOptionalUser) {
|
||||||
let bookingTeamEventType = false;
|
let bookingTeamEventType = false;
|
||||||
try {
|
try {
|
||||||
const eventType = await this.getBookedEventType(body);
|
const eventType = await this.getBookedEventType(body);
|
||||||
@@ -108,6 +120,7 @@ export class BookingsService_2024_08_13 {
|
|||||||
if (!eventType) {
|
if (!eventType) {
|
||||||
this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body);
|
this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body);
|
||||||
}
|
}
|
||||||
|
await this.checkBookingRequiresAuthenticationSetting(eventType, authUser);
|
||||||
|
|
||||||
if (eventType.schedulingType === "MANAGED") {
|
if (eventType.schedulingType === "MANAGED") {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
@@ -142,6 +155,56 @@ export class BookingsService_2024_08_13 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkBookingRequiresAuthenticationSetting(
|
||||||
|
eventType: EventTypeWithOwnerAndTeam,
|
||||||
|
authUser: AuthOptionalUser
|
||||||
|
) {
|
||||||
|
if (!eventType.bookingRequiresAuthentication) return true;
|
||||||
|
if (!authUser) {
|
||||||
|
throw new UnauthorizedException(
|
||||||
|
"checkBookingRequiresAuthentication - request must be authenticated by passing credentials belonging to event type owner, host or team or org admin or owner."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const authUserId = authUser.id;
|
||||||
|
const authUserRole = authUser.role;
|
||||||
|
const eventTypeId = eventType.id;
|
||||||
|
const teamId = eventType.teamId;
|
||||||
|
const bookingUserId = eventType.owner?.id || null;
|
||||||
|
|
||||||
|
if (authUserRole === "ADMIN") return;
|
||||||
|
|
||||||
|
if (bookingUserId === authUserId) return;
|
||||||
|
|
||||||
|
if (eventTypeId) {
|
||||||
|
const [isUserHost, isUserAssigned] = await Promise.all([
|
||||||
|
this.eventTypesRepository.isUserHostOfEventType(authUserId, eventTypeId),
|
||||||
|
this.eventTypesRepository.isUserAssignedToEventType(authUserId, eventTypeId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (isUserHost || isUserAssigned) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (teamId) {
|
||||||
|
const membership = await this.membershipsRepository.getUserAdminOrOwnerTeamMembership(
|
||||||
|
authUserId,
|
||||||
|
teamId
|
||||||
|
);
|
||||||
|
if (membership) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
bookingUserId &&
|
||||||
|
(await this.membershipsService.isUserOrgAdminOrOwnerOfAnotherUser(authUserId, bookingUserId))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 getBookedEventType(body: CreateBookingInput) {
|
async getBookedEventType(body: CreateBookingInput) {
|
||||||
if (body.eventTypeId) {
|
if (body.eventTypeId) {
|
||||||
return await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(body.eventTypeId);
|
return await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(body.eventTypeId);
|
||||||
|
|||||||
@@ -420,6 +420,9 @@ export class InputBookingsService_2024_08_13 {
|
|||||||
throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} is not a recurring event`);
|
throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} is not a recurring event`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.validateBookingLengthInMinutes(inputBooking, eventType);
|
||||||
|
const lengthInMinutes = inputBooking.lengthInMinutes ?? eventType.length;
|
||||||
|
|
||||||
const occurrence = recurringEventSchema.parse(eventType.recurringEvent);
|
const occurrence = recurringEventSchema.parse(eventType.recurringEvent);
|
||||||
const repeatsEvery = occurrence.interval;
|
const repeatsEvery = occurrence.interval;
|
||||||
|
|
||||||
@@ -456,7 +459,7 @@ export class InputBookingsService_2024_08_13 {
|
|||||||
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
|
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
|
||||||
|
|
||||||
for (let i = 0; i < repeatsTimes; i++) {
|
for (let i = 0; i < repeatsTimes; i++) {
|
||||||
const endTime = startTime.plus({ minutes: eventType.length });
|
const endTime = startTime.plus({ minutes: lengthInMinutes });
|
||||||
|
|
||||||
events.push({
|
events.push({
|
||||||
start: startTime.toISO(),
|
start: startTime.toISO(),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||||
import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard";
|
import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard";
|
||||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|||||||
+5
-1
@@ -498,6 +498,7 @@ describe("Event types Endpoints", () => {
|
|||||||
lightThemeHex: "#fafafa",
|
lightThemeHex: "#fafafa",
|
||||||
},
|
},
|
||||||
customName: `{Event type title} between {Organiser} and {Scheduler}`,
|
customName: `{Event type title} between {Organiser} and {Scheduler}`,
|
||||||
|
bookingRequiresAuthentication: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
@@ -570,6 +571,7 @@ describe("Event types Endpoints", () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
|
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
|
||||||
|
expect(createdEventType.bookingRequiresAuthentication).toEqual(true);
|
||||||
eventType = responseBody.data;
|
eventType = responseBody.data;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1026,6 +1028,7 @@ describe("Event types Endpoints", () => {
|
|||||||
lightThemeHex: "#fafafa",
|
lightThemeHex: "#fafafa",
|
||||||
},
|
},
|
||||||
customName: `{Event type title} betweennnnnnnnnnn {Organiser} and {Scheduler}`,
|
customName: `{Event type title} betweennnnnnnnnnn {Organiser} and {Scheduler}`,
|
||||||
|
bookingRequiresAuthentication: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
@@ -1120,6 +1123,8 @@ describe("Event types Endpoints", () => {
|
|||||||
eventType.color = updatedEventType.color;
|
eventType.color = updatedEventType.color;
|
||||||
eventType.bookingFields = updatedEventType.bookingFields;
|
eventType.bookingFields = updatedEventType.bookingFields;
|
||||||
eventType.calVideoSettings = updatedEventType.calVideoSettings;
|
eventType.calVideoSettings = updatedEventType.calVideoSettings;
|
||||||
|
|
||||||
|
expect(updatedEventType.bookingRequiresAuthentication).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2028,7 +2033,6 @@ describe("Event types Endpoints", () => {
|
|||||||
const username = name;
|
const username = name;
|
||||||
let user: User;
|
let user: User;
|
||||||
let legacyEventTypeId1: number;
|
let legacyEventTypeId1: number;
|
||||||
let legacyEventTypeId2: number;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withApiAuth(
|
const moduleRef = await withApiAuth(
|
||||||
|
|||||||
@@ -132,4 +132,26 @@ export class EventTypesRepository_2024_06_14 {
|
|||||||
async deleteEventType(eventTypeId: number) {
|
async deleteEventType(eventTypeId: number) {
|
||||||
return this.dbWrite.prisma.eventType.delete({ where: { id: eventTypeId } });
|
return this.dbWrite.prisma.eventType.delete({ where: { id: eventTypeId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async isUserHostOfEventType(userId: number, eventTypeId: number) {
|
||||||
|
const eventType = await this.dbRead.prisma.eventType.findFirst({
|
||||||
|
where: {
|
||||||
|
id: eventTypeId,
|
||||||
|
hosts: { some: { userId: userId } },
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
return !!eventType;
|
||||||
|
}
|
||||||
|
|
||||||
|
async isUserAssignedToEventType(userId: number, eventTypeId: number) {
|
||||||
|
const eventType = await this.dbRead.prisma.eventType.findFirst({
|
||||||
|
where: {
|
||||||
|
id: eventTypeId,
|
||||||
|
users: { some: { id: userId } },
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
return !!eventType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -90,6 +90,7 @@ type Input = Pick<
|
|||||||
| "hideOrganizerEmail"
|
| "hideOrganizerEmail"
|
||||||
| "calVideoSettings"
|
| "calVideoSettings"
|
||||||
| "hidden"
|
| "hidden"
|
||||||
|
| "bookingRequiresAuthentication"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -129,6 +130,7 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
hideOrganizerEmail,
|
hideOrganizerEmail,
|
||||||
calVideoSettings,
|
calVideoSettings,
|
||||||
hidden,
|
hidden,
|
||||||
|
bookingRequiresAuthentication,
|
||||||
} = databaseEventType;
|
} = databaseEventType;
|
||||||
|
|
||||||
const locations = this.transformLocations(databaseEventType.locations);
|
const locations = this.transformLocations(databaseEventType.locations);
|
||||||
@@ -206,6 +208,7 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
hideOrganizerEmail,
|
hideOrganizerEmail,
|
||||||
calVideoSettings,
|
calVideoSettings,
|
||||||
hidden,
|
hidden,
|
||||||
|
bookingRequiresAuthentication,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import { AtomsConferencingAppsController } from "@/modules/atoms/controllers/ato
|
|||||||
import { AtomsController } from "@/modules/atoms/controllers/atoms.controller";
|
import { AtomsController } from "@/modules/atoms/controllers/atoms.controller";
|
||||||
import { AtomsEventTypesController } from "@/modules/atoms/controllers/atoms.event-types.controller";
|
import { AtomsEventTypesController } from "@/modules/atoms/controllers/atoms.event-types.controller";
|
||||||
import { AtomsSchedulesController } from "@/modules/atoms/controllers/atoms.schedules.controller";
|
import { AtomsSchedulesController } from "@/modules/atoms/controllers/atoms.schedules.controller";
|
||||||
|
import { AtomsVerificationController } from "@/modules/atoms/controllers/atoms.verification.controller";
|
||||||
import { AttributesAtomsService } from "@/modules/atoms/services/attributes-atom.service";
|
import { AttributesAtomsService } from "@/modules/atoms/services/attributes-atom.service";
|
||||||
import { ConferencingAtomsService } from "@/modules/atoms/services/conferencing-atom.service";
|
import { ConferencingAtomsService } from "@/modules/atoms/services/conferencing-atom.service";
|
||||||
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
|
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
|
||||||
import { SchedulesAtomsService } from "@/modules/atoms/services/schedules-atom.service";
|
import { SchedulesAtomsService } from "@/modules/atoms/services/schedules-atom.service";
|
||||||
|
import { VerificationAtomsService } from "@/modules/atoms/services/verification-atom.service";
|
||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||||
@@ -32,6 +34,7 @@ import { Module } from "@nestjs/common";
|
|||||||
AtomsRepository,
|
AtomsRepository,
|
||||||
UsersService,
|
UsersService,
|
||||||
SchedulesAtomsService,
|
SchedulesAtomsService,
|
||||||
|
VerificationAtomsService,
|
||||||
RedisService,
|
RedisService,
|
||||||
],
|
],
|
||||||
exports: [EventTypesAtomService],
|
exports: [EventTypesAtomService],
|
||||||
@@ -40,6 +43,7 @@ import { Module } from "@nestjs/common";
|
|||||||
AtomsEventTypesController,
|
AtomsEventTypesController,
|
||||||
AtomsConferencingAppsController,
|
AtomsConferencingAppsController,
|
||||||
AtomsSchedulesController,
|
AtomsSchedulesController,
|
||||||
|
AtomsVerificationController,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AtomsModule {}
|
export class AtomsModule {}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
|
import { Throttle } from "@/lib/endpoint-throttler-decorator";
|
||||||
|
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
|
||||||
|
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
|
||||||
|
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
|
||||||
|
import { SendVerificationEmailOutput } from "@/modules/atoms/outputs/send-verification-email.output";
|
||||||
|
import { VerifyEmailCodeOutput } from "@/modules/atoms/outputs/verify-email-code.output";
|
||||||
|
import { VerificationAtomsService } from "@/modules/atoms/services/verification-atom.service";
|
||||||
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
UseGuards,
|
||||||
|
Version,
|
||||||
|
VERSION_NEUTRAL,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Get,
|
||||||
|
Query,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { ApiResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/atoms",
|
||||||
|
version: API_VERSIONS_VALUES,
|
||||||
|
})
|
||||||
|
@DocsTags("Atoms - verification endpoints for atoms")
|
||||||
|
@DocsExcludeController(true)
|
||||||
|
export class AtomsVerificationController {
|
||||||
|
constructor(private readonly verificationService: VerificationAtomsService) {}
|
||||||
|
|
||||||
|
@Post("/verification/email/send-code")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Throttle({ limit: 3, ttl: 60000, blockDuration: 60000, name: "atoms_verification_email_send_code" })
|
||||||
|
async sendEmailVerificationCode(
|
||||||
|
@Body() body: SendVerificationEmailInput
|
||||||
|
): Promise<SendVerificationEmailOutput> {
|
||||||
|
const result = await this.verificationService.sendEmailVerificationCode({
|
||||||
|
email: body.email,
|
||||||
|
username: body.username,
|
||||||
|
language: body.language,
|
||||||
|
isVerifyingEmail: body.isVerifyingEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: { sent: result.ok && !result.skipped },
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/verification/email/check")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async checkEmailVerificationRequired(
|
||||||
|
@Query() query: CheckEmailVerificationRequiredParams
|
||||||
|
): Promise<ApiResponse<boolean>> {
|
||||||
|
const required = await this.verificationService.checkEmailVerificationRequired({
|
||||||
|
email: query.email,
|
||||||
|
userSessionEmail: query.userSessionEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: required,
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/verification/email/verify-code")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async verifyEmailCode(@Body() body: VerifyEmailCodeInput): Promise<VerifyEmailCodeOutput> {
|
||||||
|
const verified = await this.verificationService.verifyEmailCodeUnAuthenticated({
|
||||||
|
email: body.email,
|
||||||
|
code: body.code,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: { verified },
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/verification/email/verify-code-authenticated")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async verifyEmailCodeAuthenticated(
|
||||||
|
@Body() body: VerifyEmailCodeInput,
|
||||||
|
@GetUser() user: UserWithProfile
|
||||||
|
): Promise<VerifyEmailCodeOutput> {
|
||||||
|
const verified = await this.verificationService.verifyEmailCodeAuthenticated(user, {
|
||||||
|
email: body.email,
|
||||||
|
code: body.code,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: { verified },
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsEmail, IsOptional, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class CheckEmailVerificationRequiredParams {
|
||||||
|
@ApiProperty({ example: "user@example.com" })
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: "user@example.com" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
userSessionEmail?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsEmail, IsOptional, IsString, IsBoolean } from "class-validator";
|
||||||
|
|
||||||
|
export class SendVerificationEmailInput {
|
||||||
|
@ApiProperty({ example: "user@example.com" })
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: "johndoe" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: "en" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
language?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isVerifyingEmail?: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsEmail, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class VerifyEmailCodeInput {
|
||||||
|
@ApiProperty({ example: "user@example.com" })
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "123456" })
|
||||||
|
@IsString()
|
||||||
|
code!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import { IsString, ValidateNested, IsBoolean } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class SendVerificationEmailData {
|
||||||
|
@ApiProperty({ example: true })
|
||||||
|
@IsBoolean()
|
||||||
|
@Expose()
|
||||||
|
readonly sent!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SendVerificationEmailOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
readonly status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => SendVerificationEmailData)
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty({ type: SendVerificationEmailData })
|
||||||
|
readonly data!: SendVerificationEmailData;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import { IsString, ValidateNested, IsBoolean } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class VerifyEmailCodeData {
|
||||||
|
@ApiProperty({ example: true })
|
||||||
|
@IsBoolean()
|
||||||
|
@Expose()
|
||||||
|
readonly verified!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VerifyEmailCodeOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
readonly status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => VerifyEmailCodeData)
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty({ type: VerifyEmailCodeData })
|
||||||
|
readonly data!: VerifyEmailCodeData;
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
|
||||||
|
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
|
||||||
|
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
|
||||||
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
|
import { Injectable, BadRequestException, UnauthorizedException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import {
|
||||||
|
verifyCodeUnAuthenticated,
|
||||||
|
verifyCodeAuthenticated,
|
||||||
|
sendEmailVerificationByCode,
|
||||||
|
checkEmailVerificationRequired,
|
||||||
|
} from "@calcom/platform-libraries";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class VerificationAtomsService {
|
||||||
|
async checkEmailVerificationRequired(input: CheckEmailVerificationRequiredParams) {
|
||||||
|
return await checkEmailVerificationRequired(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyEmailCodeUnAuthenticated(input: VerifyEmailCodeInput) {
|
||||||
|
try {
|
||||||
|
return await verifyCodeUnAuthenticated(input);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if (error.message === "invalid_code") {
|
||||||
|
throw new BadRequestException("Invalid verification code");
|
||||||
|
}
|
||||||
|
if (error.message === "BAD_REQUEST") {
|
||||||
|
throw new BadRequestException("Email and code are required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new BadRequestException("Verification failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyEmailCodeAuthenticated(user: UserWithProfile, input: VerifyEmailCodeInput) {
|
||||||
|
try {
|
||||||
|
return await verifyCodeAuthenticated({
|
||||||
|
user,
|
||||||
|
email: input.email,
|
||||||
|
code: input.code,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if (error.message === "invalid_code") {
|
||||||
|
throw new BadRequestException("Invalid verification code");
|
||||||
|
}
|
||||||
|
if (error.message === "BAD_REQUEST") {
|
||||||
|
throw new BadRequestException("Email, code, and user ID are required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new UnauthorizedException("Verification failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendEmailVerificationCode(input: SendVerificationEmailInput) {
|
||||||
|
return await sendEmailVerificationByCode({
|
||||||
|
email: input.email,
|
||||||
|
username: input.username,
|
||||||
|
language: input.language,
|
||||||
|
isVerifyingEmail: input.isVerifyingEmail,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import { NextAuthStrategy } from "@/modules/auth/strategies/next-auth/next-auth.
|
|||||||
import { DeploymentsModule } from "@/modules/deployments/deployments.module";
|
import { DeploymentsModule } from "@/modules/deployments/deployments.module";
|
||||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
import { ProfilesModule } from "@/modules/profiles/profiles.module";
|
|
||||||
import { RedisModule } from "@/modules/redis/redis.module";
|
import { RedisModule } from "@/modules/redis/redis.module";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
@@ -22,7 +21,6 @@ import { PassportModule } from "@nestjs/passport";
|
|||||||
MembershipsModule,
|
MembershipsModule,
|
||||||
TokensModule,
|
TokensModule,
|
||||||
DeploymentsModule,
|
DeploymentsModule,
|
||||||
ProfilesModule,
|
|
||||||
],
|
],
|
||||||
providers: [NextAuthGuard, NextAuthStrategy, ApiAuthGuard, ApiAuthStrategy, OAuthFlowService],
|
providers: [NextAuthGuard, NextAuthStrategy, ApiAuthGuard, ApiAuthStrategy, OAuthFlowService],
|
||||||
exports: [NextAuthGuard, ApiAuthGuard],
|
exports: [NextAuthGuard, ApiAuthGuard],
|
||||||
|
|||||||
+2
@@ -2,6 +2,8 @@ import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.st
|
|||||||
import { ExecutionContext } from "@nestjs/common";
|
import { ExecutionContext } from "@nestjs/common";
|
||||||
import { createParamDecorator } from "@nestjs/common";
|
import { createParamDecorator } from "@nestjs/common";
|
||||||
|
|
||||||
|
export type AuthOptionalUser = ApiAuthGuardUser | null;
|
||||||
|
|
||||||
export const GetOptionalUser = createParamDecorator<
|
export const GetOptionalUser = createParamDecorator<
|
||||||
keyof ApiAuthGuardUser | (keyof ApiAuthGuardUser)[],
|
keyof ApiAuthGuardUser | (keyof ApiAuthGuardUser)[],
|
||||||
ExecutionContext
|
ExecutionContext
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
|||||||
import { DeploymentsRepository } from "@/modules/deployments/deployments.repository";
|
import { DeploymentsRepository } from "@/modules/deployments/deployments.repository";
|
||||||
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||||
import { JwtService } from "@/modules/jwt/jwt.service";
|
import { JwtService } from "@/modules/jwt/jwt.service";
|
||||||
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
@@ -20,6 +21,7 @@ import { Test, TestingModule } from "@nestjs/testing";
|
|||||||
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||||
import { createRequest } from "node-mocks-http";
|
import { createRequest } from "node-mocks-http";
|
||||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||||
|
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
@@ -45,6 +47,7 @@ describe("ApiAuthStrategy", () => {
|
|||||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||||
let oAuthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
let oAuthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||||
|
let membershipRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
|
||||||
const validApiKeyEmail = `api-auth-api-key-user-${randomString()}@api.com`;
|
const validApiKeyEmail = `api-auth-api-key-user-${randomString()}@api.com`;
|
||||||
const validAccessTokenEmail = `api-auth-access-token-user-${randomString()}@api.com`;
|
const validAccessTokenEmail = `api-auth-access-token-user-${randomString()}@api.com`;
|
||||||
@@ -65,6 +68,7 @@ describe("ApiAuthStrategy", () => {
|
|||||||
}),
|
}),
|
||||||
ProfilesModule,
|
ProfilesModule,
|
||||||
TokensModule,
|
TokensModule,
|
||||||
|
MembershipsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
MockedRedisService,
|
MockedRedisService,
|
||||||
@@ -91,6 +95,7 @@ describe("ApiAuthStrategy", () => {
|
|||||||
teamRepositoryFixture = new TeamRepositoryFixture(module);
|
teamRepositoryFixture = new TeamRepositoryFixture(module);
|
||||||
oAuthClientRepositoryFixture = new OAuthClientRepositoryFixture(module);
|
oAuthClientRepositoryFixture = new OAuthClientRepositoryFixture(module);
|
||||||
profilesRepositoryFixture = new ProfileRepositoryFixture(module);
|
profilesRepositoryFixture = new ProfileRepositoryFixture(module);
|
||||||
|
membershipRepositoryFixture = new MembershipRepositoryFixture(module);
|
||||||
organization = await teamRepositoryFixture.create({ name: `api-auth-organization-1-${randomString()}` });
|
organization = await teamRepositoryFixture.create({ name: `api-auth-organization-1-${randomString()}` });
|
||||||
organizationTwo = await teamRepositoryFixture.create({
|
organizationTwo = await teamRepositoryFixture.create({
|
||||||
name: `api-auth-organization-2-${randomString()}`,
|
name: `api-auth-organization-2-${randomString()}`,
|
||||||
@@ -121,6 +126,20 @@ describe("ApiAuthStrategy", () => {
|
|||||||
organization: { connect: { id: organizationTwo.id } },
|
organization: { connect: { id: organizationTwo.id } },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await membershipRepositoryFixture.create({
|
||||||
|
user: { connect: { id: validOAuthUser.id } },
|
||||||
|
team: { connect: { id: organization.id } },
|
||||||
|
role: "OWNER",
|
||||||
|
accepted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await membershipRepositoryFixture.create({
|
||||||
|
user: { connect: { id: validOAuthUser.id } },
|
||||||
|
team: { connect: { id: organizationTwo.id } },
|
||||||
|
role: "OWNER",
|
||||||
|
accepted: true,
|
||||||
|
});
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
logo: "logo-url",
|
logo: "logo-url",
|
||||||
name: "name",
|
name: "name",
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed";
|
|||||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
||||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||||
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||||
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
import { ProfilesRepository } from "@/modules/profiles/profiles.repository";
|
|
||||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||||
import { TokensService } from "@/modules/tokens/tokens.service";
|
import { TokensService } from "@/modules/tokens/tokens.service";
|
||||||
import { UsersService } from "@/modules/users/services/users.service";
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
@@ -45,8 +45,8 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|||||||
private readonly userRepository: UsersRepository,
|
private readonly userRepository: UsersRepository,
|
||||||
private readonly apiKeyRepository: ApiKeysRepository,
|
private readonly apiKeyRepository: ApiKeysRepository,
|
||||||
private readonly oauthRepository: OAuthClientRepository,
|
private readonly oauthRepository: OAuthClientRepository,
|
||||||
private readonly profilesRepository: ProfilesRepository,
|
private readonly usersService: UsersService,
|
||||||
private readonly usersService: UsersService
|
private readonly membershipsRepository: MembershipsRepository
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -172,7 +172,9 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
|||||||
throw new UnauthorizedException("ApiAuthStrategy - oAuth client - Invalid client secret");
|
throw new UnauthorizedException("ApiAuthStrategy - oAuth client - Invalid client secret");
|
||||||
}
|
}
|
||||||
|
|
||||||
const platformCreatorId = await this.profilesRepository.getPlatformOwnerUserId(client.organizationId);
|
const platformCreatorId =
|
||||||
|
(await this.membershipsRepository.findPlatformOwnerUserId(client.organizationId)) ||
|
||||||
|
(await this.membershipsRepository.findPlatformAdminUserId(client.organizationId));
|
||||||
|
|
||||||
if (!platformCreatorId) {
|
if (!platformCreatorId) {
|
||||||
throw new UnauthorizedException(
|
throw new UnauthorizedException(
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ export class BillingService implements OnModuleDestroy {
|
|||||||
teamId: teamId.toString(),
|
teamId: teamId.toString(),
|
||||||
plan: plan.toString(),
|
plan: plan.toString(),
|
||||||
},
|
},
|
||||||
currency: "usd",
|
|
||||||
subscription_data: {
|
subscription_data: {
|
||||||
metadata: {
|
metadata: {
|
||||||
teamId: teamId.toString(),
|
teamId: teamId.toString(),
|
||||||
@@ -117,7 +116,6 @@ export class BillingService implements OnModuleDestroy {
|
|||||||
teamId: teamId.toString(),
|
teamId: teamId.toString(),
|
||||||
plan: plan.toString(),
|
plan: plan.toString(),
|
||||||
},
|
},
|
||||||
currency: "usd",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!url) throw new InternalServerErrorException("Failed to create Stripe session.");
|
if (!url) throw new InternalServerErrorException("Failed to create Stripe session.");
|
||||||
@@ -403,7 +401,6 @@ export class BillingService implements OnModuleDestroy {
|
|||||||
fromReschedule?: string | null;
|
fromReschedule?: string | null;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
||||||
if (this.configService.get("e2e")) {
|
if (this.configService.get("e2e")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -465,7 +462,6 @@ export class BillingService implements OnModuleDestroy {
|
|||||||
|
|
||||||
async onModuleDestroy() {
|
async onModuleDestroy() {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
await this.billingQueue.close();
|
await this.billingQueue.close();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
import { MembershipRole } from "@calcom/prisma/client";
|
import { MembershipRole } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MembershipsRepository {
|
export class MembershipsRepository {
|
||||||
@@ -20,6 +20,36 @@ export class MembershipsRepository {
|
|||||||
return membership;
|
return membership;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findPlatformOwnerUserId(organizationId: number): Promise<number | undefined> {
|
||||||
|
const owner = await this.dbRead.prisma.membership.findFirst({
|
||||||
|
where: {
|
||||||
|
teamId: organizationId,
|
||||||
|
role: "OWNER",
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return owner?.userId ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findPlatformAdminUserId(organizationId: number): Promise<number | undefined> {
|
||||||
|
const admin = await this.dbRead.prisma.membership.findFirst({
|
||||||
|
where: {
|
||||||
|
teamId: organizationId,
|
||||||
|
role: "ADMIN",
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return admin?.userId ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
async findMembershipByTeamId(teamId: number, userId: number) {
|
async findMembershipByTeamId(teamId: number, userId: number) {
|
||||||
const membership = await this.dbRead.prisma.membership.findUnique({
|
const membership = await this.dbRead.prisma.membership.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@@ -60,6 +90,26 @@ export class MembershipsRepository {
|
|||||||
return !!adminMembership;
|
return !!adminMembership;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getOrgIdsWhereUserIsAdminOrOwner(userId: number) {
|
||||||
|
const userOrgAdminMemberships = await this.dbRead.prisma.membership.findMany({
|
||||||
|
where: {
|
||||||
|
userId: userId,
|
||||||
|
accepted: true,
|
||||||
|
role: {
|
||||||
|
in: ["OWNER", "ADMIN"],
|
||||||
|
},
|
||||||
|
team: {
|
||||||
|
parentId: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
teamId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return userOrgAdminMemberships.map((m) => m.teamId);
|
||||||
|
}
|
||||||
|
|
||||||
async createMembership(teamId: number, userId: number, role: MembershipRole, accepted: boolean) {
|
async createMembership(teamId: number, userId: number, role: MembershipRole, accepted: boolean) {
|
||||||
const membership = await this.dbRead.prisma.membership.create({
|
const membership = await this.dbRead.prisma.membership.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -73,4 +123,46 @@ export class MembershipsRepository {
|
|||||||
|
|
||||||
return membership;
|
return membership;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserAdminOrOwnerTeamMembership(userId: number, teamId: number) {
|
||||||
|
return this.dbRead.prisma.membership.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: userId,
|
||||||
|
teamId: teamId,
|
||||||
|
accepted: true,
|
||||||
|
role: {
|
||||||
|
in: ["OWNER", "ADMIN"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserMembershipInOneOfOrgs(userId: number, orgIds: number[]) {
|
||||||
|
return this.dbRead.prisma.membership.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: userId,
|
||||||
|
accepted: true,
|
||||||
|
teamId: {
|
||||||
|
in: orgIds,
|
||||||
|
},
|
||||||
|
team: {
|
||||||
|
parentId: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserMembershipInOneOfOrgsTeams(userId: number, orgIds: number[]) {
|
||||||
|
return this.dbRead.prisma.membership.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: userId,
|
||||||
|
accepted: true,
|
||||||
|
team: {
|
||||||
|
parentId: {
|
||||||
|
in: orgIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,4 +21,28 @@ export class MembershipsService {
|
|||||||
"teamId"
|
"teamId"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async isUserOrgAdminOrOwnerOfAnotherUser(userId: number, anotherUserId: number) {
|
||||||
|
const orgIdsWhereUserIsAdminOrOwner = await this.membershipsRepository.getOrgIdsWhereUserIsAdminOrOwner(
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (orgIdsWhereUserIsAdminOrOwner.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const anotherUserOrgMembership = await this.membershipsRepository.getUserMembershipInOneOfOrgs(
|
||||||
|
anotherUserId,
|
||||||
|
orgIdsWhereUserIsAdminOrOwner
|
||||||
|
);
|
||||||
|
|
||||||
|
if (anotherUserOrgMembership) return true;
|
||||||
|
|
||||||
|
const anotherUserOrgTeamMembership = await this.membershipsRepository.getUserMembershipInOneOfOrgsTeams(
|
||||||
|
anotherUserId,
|
||||||
|
orgIdsWhereUserIsAdminOrOwner
|
||||||
|
);
|
||||||
|
|
||||||
|
return !!anotherUserOrgTeamMembership;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -15,6 +15,7 @@ import { NestExpressApplication } from "@nestjs/platform-express";
|
|||||||
import { Test, TestingModule } from "@nestjs/testing";
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||||
import * as request from "supertest";
|
import * as request from "supertest";
|
||||||
|
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||||
@@ -63,6 +64,7 @@ describe("OAuthFlow Endpoints", () => {
|
|||||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||||
let oAuthClientsRepositoryFixture: OAuthClientRepositoryFixture;
|
let oAuthClientsRepositoryFixture: OAuthClientRepositoryFixture;
|
||||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||||
|
let membershipRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
|
||||||
let user: User;
|
let user: User;
|
||||||
let organization: Team;
|
let organization: Team;
|
||||||
@@ -89,6 +91,7 @@ describe("OAuthFlow Endpoints", () => {
|
|||||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||||
usersRepositoryFixtures = new UserRepositoryFixture(moduleRef);
|
usersRepositoryFixtures = new UserRepositoryFixture(moduleRef);
|
||||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||||
|
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
user = await usersRepositoryFixtures.create({
|
user = await usersRepositoryFixtures.create({
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
@@ -104,6 +107,13 @@ describe("OAuthFlow Endpoints", () => {
|
|||||||
movedFromUser: { connect: { id: user.id } },
|
movedFromUser: { connect: { id: user.id } },
|
||||||
organization: { connect: { id: organization.id } },
|
organization: { connect: { id: organization.id } },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await membershipRepositoryFixture.create({
|
||||||
|
user: { connect: { id: user.id } },
|
||||||
|
team: { connect: { id: organization.id } },
|
||||||
|
role: "OWNER",
|
||||||
|
accepted: true,
|
||||||
|
});
|
||||||
oAuthClient = await createOAuthClient(organization.id);
|
oAuthClient = await createOAuthClient(organization.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+7
@@ -39,6 +39,7 @@ import {
|
|||||||
HttpStatus,
|
HttpStatus,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
Query,
|
Query,
|
||||||
|
Logger,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
@@ -66,6 +67,8 @@ export type EventTypeHandlerResponse = {
|
|||||||
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||||
@ApiHeader(OPTIONAL_API_KEY_HEADER)
|
@ApiHeader(OPTIONAL_API_KEY_HEADER)
|
||||||
export class OrganizationsEventTypesController {
|
export class OrganizationsEventTypesController {
|
||||||
|
private readonly logger = new Logger("OrganizationsEventTypesController");
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly organizationsEventTypesService: OrganizationsEventTypesService,
|
private readonly organizationsEventTypesService: OrganizationsEventTypesService,
|
||||||
private readonly inputService: InputOrganizationsEventTypesService,
|
private readonly inputService: InputOrganizationsEventTypesService,
|
||||||
@@ -88,6 +91,10 @@ export class OrganizationsEventTypesController {
|
|||||||
teamId,
|
teamId,
|
||||||
bodyEventType
|
bodyEventType
|
||||||
);
|
);
|
||||||
|
this.logger.debug(
|
||||||
|
"nl debug - create org team event type - transformedBody",
|
||||||
|
JSON.stringify(transformedBody, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
const eventType = await this.organizationsEventTypesService.createOrganizationTeamEventType(
|
const eventType = await this.organizationsEventTypesService.createOrganizationTeamEventType(
|
||||||
user,
|
user,
|
||||||
|
|||||||
+4
@@ -47,6 +47,10 @@ export class OrganizationsEventTypesService {
|
|||||||
prisma: this.dbWrite.prisma,
|
prisma: this.dbWrite.prisma,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
this.logger.debug(
|
||||||
|
"nl debug - create org team event type - eventTypeCreated",
|
||||||
|
JSON.stringify(eventTypeCreated, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
return this.teamsEventTypesService.updateTeamEventType(eventTypeCreated.id, teamId, body, user, true);
|
return this.teamsEventTypesService.updateTeamEventType(eventTypeCreated.id, teamId, body, user, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ type Input = Pick<
|
|||||||
| "team"
|
| "team"
|
||||||
| "calVideoSettings"
|
| "calVideoSettings"
|
||||||
| "hidden"
|
| "hidden"
|
||||||
|
| "bookingRequiresAuthentication"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
+5
@@ -43,6 +43,8 @@ export class OrganizationsTeamsSchedulesController {
|
|||||||
@DocsTags("Orgs / Teams / Schedules")
|
@DocsTags("Orgs / Teams / Schedules")
|
||||||
@ApiOperation({ summary: "Get all team member schedules" })
|
@ApiOperation({ summary: "Get all team member schedules" })
|
||||||
async getTeamSchedules(
|
async getTeamSchedules(
|
||||||
|
// note(Lauirs): putting orgId so swagger is generacted correctly
|
||||||
|
@Param("orgId", ParseIntPipe) _orgId: number,
|
||||||
@Param("teamId", ParseIntPipe) teamId: number,
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
@Query() queryParams: SkipTakePagination
|
@Query() queryParams: SkipTakePagination
|
||||||
): Promise<GetSchedulesOutput_2024_06_11> {
|
): Promise<GetSchedulesOutput_2024_06_11> {
|
||||||
@@ -65,6 +67,9 @@ export class OrganizationsTeamsSchedulesController {
|
|||||||
summary: "Get schedules of a team member",
|
summary: "Get schedules of a team member",
|
||||||
})
|
})
|
||||||
async getUserSchedules(
|
async getUserSchedules(
|
||||||
|
// note(Lauirs): putting orgId and teamId so swagger is generacted correctly
|
||||||
|
@Param("orgId", ParseIntPipe) _orgId: number,
|
||||||
|
@Param("teamId", ParseIntPipe) _teamId: number,
|
||||||
@Param("userId", ParseIntPipe) userId: number
|
@Param("userId", ParseIntPipe) userId: number
|
||||||
): Promise<GetSchedulesOutput_2024_06_11> {
|
): Promise<GetSchedulesOutput_2024_06_11> {
|
||||||
const schedules = await this.schedulesService.getUserSchedules(userId);
|
const schedules = await this.schedulesService.getUserSchedules(userId);
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { VERSION_2024_09_04 } from "@/lib/api-versions";
|
import { VERSION_2024_09_04 } from "@/lib/api-versions";
|
||||||
import { OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER, OPTIONAL_X_CAL_CLIENT_ID_HEADER } from "@/lib/docs/headers";
|
import { OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER, OPTIONAL_X_CAL_CLIENT_ID_HEADER } from "@/lib/docs/headers";
|
||||||
import { GetOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
|
import {
|
||||||
|
AuthOptionalUser,
|
||||||
|
GetOptionalUser,
|
||||||
|
} from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
|
||||||
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
|
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
|
||||||
import { GetReservedSlotOutput_2024_09_04 } from "@/modules/slots/slots-2024-09-04/outputs/get-reserved-slot.output";
|
import { GetReservedSlotOutput_2024_09_04 } from "@/modules/slots/slots-2024-09-04/outputs/get-reserved-slot.output";
|
||||||
import { GetSlotsOutput_2024_09_04 } from "@/modules/slots/slots-2024-09-04/outputs/get-slots.output";
|
import { GetSlotsOutput_2024_09_04 } from "@/modules/slots/slots-2024-09-04/outputs/get-slots.output";
|
||||||
@@ -26,7 +29,6 @@ import {
|
|||||||
ApiResponse as DocsResponse,
|
ApiResponse as DocsResponse,
|
||||||
ApiQuery,
|
ApiQuery,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import { User } from "@prisma/client";
|
|
||||||
import { plainToClass } from "class-transformer";
|
import { plainToClass } from "class-transformer";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
@@ -261,7 +263,7 @@ export class SlotsController_2024_09_04 {
|
|||||||
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
async reserveSlot(
|
async reserveSlot(
|
||||||
@Body() body: ReserveSlotInput_2024_09_04,
|
@Body() body: ReserveSlotInput_2024_09_04,
|
||||||
@GetOptionalUser() user: User
|
@GetOptionalUser() user: AuthOptionalUser
|
||||||
): Promise<ReserveSlotOutputResponse_2024_09_04> {
|
): Promise<ReserveSlotOutputResponse_2024_09_04> {
|
||||||
const reservedSlot = await this.slotsService.reserveSlot(body, user?.id);
|
const reservedSlot = await this.slotsService.reserveSlot(body, user?.id);
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ export class TeamsEventTypesService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const eventType = await this.teamsEventTypesRepository.getEventTypeById(eventTypeId);
|
const eventType = await this.teamsEventTypesRepository.getEventTypeById(eventTypeId);
|
||||||
|
this.logger.debug("nl debug - update team event type - eventType", JSON.stringify(eventType, null, 2));
|
||||||
|
|
||||||
if (!eventType) {
|
if (!eventType) {
|
||||||
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||||
|
|||||||
@@ -5547,6 +5547,97 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v2/organizations/{orgId}/teams/{teamId}/schedules": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "OrganizationsTeamsSchedulesController_getTeamSchedules",
|
||||||
|
"summary": "Get all team member schedules",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-secret-key",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client secret key",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-cal-client-id",
|
||||||
|
"in": "header",
|
||||||
|
"description": "For platform customers - OAuth client ID",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "orgId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "take",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"description": "Maximum number of items to return",
|
||||||
|
"example": 25,
|
||||||
|
"schema": {
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 250,
|
||||||
|
"default": 250,
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "skip",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"description": "Number of items to skip",
|
||||||
|
"example": 0,
|
||||||
|
"schema": {
|
||||||
|
"minimum": 0,
|
||||||
|
"default": 0,
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Orgs / Teams / Schedules"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v2/organizations/{orgId}/teams/{teamId}/stripe/connect": {
|
"/v2/organizations/{orgId}/teams/{teamId}/stripe/connect": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "OrganizationsStripeController_getTeamStripeConnectUrl",
|
"operationId": "OrganizationsStripeController_getTeamStripeConnectUrl",
|
||||||
@@ -5720,6 +5811,22 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "orgId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "userId",
|
"name": "userId",
|
||||||
"required": true,
|
"required": true,
|
||||||
@@ -13215,6 +13322,71 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v2/teams/{teamId}/schedules": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "TeamsSchedulesController_getTeamSchedules",
|
||||||
|
"summary": "Get all team member schedules",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_ or managed user access token",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "take",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"description": "Maximum number of items to return",
|
||||||
|
"example": 25,
|
||||||
|
"schema": {
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 250,
|
||||||
|
"default": 250,
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "skip",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"description": "Number of items to skip",
|
||||||
|
"example": 0,
|
||||||
|
"schema": {
|
||||||
|
"minimum": 0,
|
||||||
|
"default": 0,
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Teams / Schedules"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v2/teams/{teamId}/verified-resources/emails/verification-code/request": {
|
"/v2/teams/{teamId}/verified-resources/emails/verification-code/request": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "TeamsVerifiedResourcesController_requestEmailVerificationCode",
|
"operationId": "TeamsVerifiedResourcesController_requestEmailVerificationCode",
|
||||||
@@ -16504,6 +16676,14 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"hidden": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"bookingRequiresAuthentication": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
|
||||||
|
},
|
||||||
"locations": {
|
"locations": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "Locations where the event will take place. If not provided, cal video link will be used as the location.",
|
"description": "Locations where the event will take place. If not provided, cal video link will be used as the location.",
|
||||||
@@ -18166,6 +18346,13 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"hidden": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"bookingRequiresAuthentication": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
|
||||||
|
},
|
||||||
"ownerId": {
|
"ownerId": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"example": 10
|
"example": 10
|
||||||
@@ -18195,6 +18382,8 @@
|
|||||||
"successRedirectUrl",
|
"successRedirectUrl",
|
||||||
"isInstantEvent",
|
"isInstantEvent",
|
||||||
"scheduleId",
|
"scheduleId",
|
||||||
|
"hidden",
|
||||||
|
"bookingRequiresAuthentication",
|
||||||
"ownerId",
|
"ownerId",
|
||||||
"users"
|
"users"
|
||||||
]
|
]
|
||||||
@@ -18515,6 +18704,14 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"hidden": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"bookingRequiresAuthentication": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
|
||||||
|
},
|
||||||
"locations": {
|
"locations": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "Locations where the event will take place. If not provided, cal video link will be used as the location.",
|
"description": "Locations where the event will take place. If not provided, cal video link will be used as the location.",
|
||||||
@@ -20343,6 +20540,14 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"hidden": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"bookingRequiresAuthentication": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
|
||||||
|
},
|
||||||
"schedulingType": {
|
"schedulingType": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -20777,6 +20982,13 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"hidden": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"bookingRequiresAuthentication": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
|
||||||
|
},
|
||||||
"teamId": {
|
"teamId": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
@@ -21223,6 +21435,14 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"hidden": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"bookingRequiresAuthentication": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
|
||||||
|
},
|
||||||
"hosts": {
|
"hosts": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
@@ -22589,6 +22809,11 @@
|
|||||||
},
|
},
|
||||||
"unit": {
|
"unit": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"enum": [
|
||||||
|
"hour",
|
||||||
|
"minute",
|
||||||
|
"day"
|
||||||
|
],
|
||||||
"description": "Unit for the offset time",
|
"description": "Unit for the offset time",
|
||||||
"example": "hour"
|
"example": "hour"
|
||||||
}
|
}
|
||||||
@@ -22612,6 +22837,9 @@
|
|||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "beforeEvent",
|
"default": "beforeEvent",
|
||||||
|
"enum": [
|
||||||
|
"beforeEvent"
|
||||||
|
],
|
||||||
"description": "Trigger type for the workflow",
|
"description": "Trigger type for the workflow",
|
||||||
"example": "beforeEvent"
|
"example": "beforeEvent"
|
||||||
}
|
}
|
||||||
@@ -22635,6 +22863,9 @@
|
|||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "afterEvent",
|
"default": "afterEvent",
|
||||||
|
"enum": [
|
||||||
|
"afterEvent"
|
||||||
|
],
|
||||||
"description": "Trigger type for the workflow",
|
"description": "Trigger type for the workflow",
|
||||||
"example": "afterEvent"
|
"example": "afterEvent"
|
||||||
}
|
}
|
||||||
@@ -22650,6 +22881,9 @@
|
|||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "eventCancelled",
|
"default": "eventCancelled",
|
||||||
|
"enum": [
|
||||||
|
"eventCancelled"
|
||||||
|
],
|
||||||
"description": "Trigger type for the workflow"
|
"description": "Trigger type for the workflow"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -22663,6 +22897,9 @@
|
|||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "newEvent",
|
"default": "newEvent",
|
||||||
|
"enum": [
|
||||||
|
"newEvent"
|
||||||
|
],
|
||||||
"description": "Trigger type for the workflow"
|
"description": "Trigger type for the workflow"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -22676,6 +22913,9 @@
|
|||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "rescheduleEvent",
|
"default": "rescheduleEvent",
|
||||||
|
"enum": [
|
||||||
|
"rescheduleEvent"
|
||||||
|
],
|
||||||
"description": "Trigger type for the workflow"
|
"description": "Trigger type for the workflow"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -22697,6 +22937,9 @@
|
|||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "afterGuestsCalVideoNoShow",
|
"default": "afterGuestsCalVideoNoShow",
|
||||||
|
"enum": [
|
||||||
|
"afterGuestsCalVideoNoShow"
|
||||||
|
],
|
||||||
"description": "Trigger type for the workflow",
|
"description": "Trigger type for the workflow",
|
||||||
"example": "afterGuestsCalVideoNoShow"
|
"example": "afterGuestsCalVideoNoShow"
|
||||||
}
|
}
|
||||||
@@ -22720,6 +22963,9 @@
|
|||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "afterHostsCalVideoNoShow",
|
"default": "afterHostsCalVideoNoShow",
|
||||||
|
"enum": [
|
||||||
|
"afterHostsCalVideoNoShow"
|
||||||
|
],
|
||||||
"description": "Trigger type for the workflow",
|
"description": "Trigger type for the workflow",
|
||||||
"example": "afterHostsCalVideoNoShow"
|
"example": "afterHostsCalVideoNoShow"
|
||||||
}
|
}
|
||||||
@@ -23314,6 +23560,17 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"enum": [
|
||||||
|
[
|
||||||
|
"beforeEvent",
|
||||||
|
"eventCancelled",
|
||||||
|
"newEvent",
|
||||||
|
"afterEvent",
|
||||||
|
"rescheduleEvent",
|
||||||
|
"afterHostsCalVideoNoShow",
|
||||||
|
"afterGuestsCalVideoNoShow"
|
||||||
|
]
|
||||||
|
],
|
||||||
"description": "Trigger type for the workflow"
|
"description": "Trigger type for the workflow"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { CustomI18nProvider } from "app/CustomI18nProvider";
|
import { CustomI18nProvider } from "app/CustomI18nProvider";
|
||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||||
import type { PageProps, SearchParams } from "app/_types";
|
import type { PageProps, Params, SearchParams } from "app/_types";
|
||||||
import { generateMeetingMetadata } from "app/_utils";
|
import { generateMeetingMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
import { cookies, headers } from "next/headers";
|
||||||
|
|
||||||
@@ -15,20 +15,34 @@ import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
|
|||||||
import LegacyPage from "~/team/type-view";
|
import LegacyPage from "~/team/type-view";
|
||||||
import type { PageProps as LegacyPageProps } from "~/team/type-view";
|
import type { PageProps as LegacyPageProps } from "~/team/type-view";
|
||||||
|
|
||||||
import CachedTeamBooker, { generateMetadata as generateCachedMetadata } from "./pageWithCachedData";
|
import CachedTeamBooker, {
|
||||||
|
generateMetadata as generateCachedMetadata,
|
||||||
|
getOrgContext,
|
||||||
|
} from "./pageWithCachedData";
|
||||||
|
import { getTeamId } from "./queries";
|
||||||
|
|
||||||
|
async function isCachedTeamBookingEnabled(params: Params, searchParams: SearchParams): Promise<boolean> {
|
||||||
|
if (searchParams.experimentalTeamBookingPageCache !== "true") return false;
|
||||||
|
|
||||||
|
const { teamSlug, currentOrgDomain, isValidOrgDomain } = await getOrgContext(params);
|
||||||
|
const orgSlug = isValidOrgDomain ? currentOrgDomain : null;
|
||||||
|
const teamId = await getTeamId(teamSlug, orgSlug);
|
||||||
|
|
||||||
|
if (!teamId) return false;
|
||||||
|
|
||||||
async function isCachedTeamBookingEnabled(searchParams: SearchParams): Promise<boolean> {
|
|
||||||
const featuresRepository = new FeaturesRepository(prisma);
|
const featuresRepository = new FeaturesRepository(prisma);
|
||||||
const isGloballyEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally(
|
const isTeamFeatureEnabled = await featuresRepository.checkIfTeamHasFeature(
|
||||||
|
teamId,
|
||||||
"team-booking-page-cache"
|
"team-booking-page-cache"
|
||||||
);
|
);
|
||||||
return isGloballyEnabled && searchParams.experimentalTeamBookingPageCache === "true";
|
return isTeamFeatureEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||||
if (await isCachedTeamBookingEnabled(await searchParams)) {
|
if (await isCachedTeamBookingEnabled(await params, await searchParams)) {
|
||||||
return await generateCachedMetadata({ params, searchParams });
|
return await generateCachedMetadata({ params, searchParams });
|
||||||
}
|
}
|
||||||
|
|
||||||
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||||
const props = await getData(legacyCtx);
|
const props = await getData(legacyCtx);
|
||||||
const { booking, isSEOIndexable, eventData, isBrandingHidden } = props;
|
const { booking, isSEOIndexable, eventData, isBrandingHidden } = props;
|
||||||
@@ -68,7 +82,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
|||||||
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||||
if (await isCachedTeamBookingEnabled(await searchParams)) {
|
if (await isCachedTeamBookingEnabled(await params, await searchParams)) {
|
||||||
return await CachedTeamBooker({ params, searchParams });
|
return await CachedTeamBooker({ params, searchParams });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const _getTeamMetadataForBooking = (teamData: NonNullable<TeamData>, eventTypeId
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
async function _getOrgContext(params: Params) {
|
export async function getOrgContext(params: Params) {
|
||||||
const result = paramsSchema.safeParse({
|
const result = paramsSchema.safeParse({
|
||||||
slug: params?.slug,
|
slug: params?.slug,
|
||||||
type: params?.type,
|
type: params?.type,
|
||||||
@@ -74,7 +74,7 @@ const _getMultipleDurationValue = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||||
const { currentOrgDomain, isValidOrgDomain, teamSlug, meetingSlug } = await _getOrgContext(await params);
|
const { currentOrgDomain, isValidOrgDomain, teamSlug, meetingSlug } = await getOrgContext(await params);
|
||||||
|
|
||||||
const teamData = await getCachedTeamData(teamSlug, currentOrgDomain);
|
const teamData = await getCachedTeamData(teamSlug, currentOrgDomain);
|
||||||
if (!teamData) return {}; // should never happen
|
if (!teamData) return {}; // should never happen
|
||||||
@@ -123,7 +123,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CachedTeamBooker = async ({ params, searchParams }: PageProps) => {
|
const CachedTeamBooker = async ({ params, searchParams }: PageProps) => {
|
||||||
const { currentOrgDomain, isValidOrgDomain, teamSlug, meetingSlug } = await _getOrgContext(await params);
|
const { currentOrgDomain, isValidOrgDomain, teamSlug, meetingSlug } = await getOrgContext(await params);
|
||||||
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||||
|
|
||||||
// Handle org redirects
|
// Handle org redirects
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { getTeamEventType } from "@calcom/features/eventtypes/lib/getTeamEventTy
|
|||||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||||
import { NEXTJS_CACHE_TTL } from "@calcom/lib/constants";
|
import { NEXTJS_CACHE_TTL } from "@calcom/lib/constants";
|
||||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||||
|
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
||||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import type { SchedulingType } from "@calcom/prisma/enums";
|
import type { SchedulingType } from "@calcom/prisma/enums";
|
||||||
@@ -158,3 +159,14 @@ export async function getCRMData(
|
|||||||
crmRecordId,
|
crmRecordId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getTeamId(teamSlug: string, orgSlug: string | null): Promise<number | null> {
|
||||||
|
const teamRepo = new TeamRepository(prisma);
|
||||||
|
const team = await teamRepo.findFirstBySlugAndParentSlug({
|
||||||
|
slug: teamSlug,
|
||||||
|
parentSlug: orgSlug,
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return team?.id ?? null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
|
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
|
||||||
import type { PageProps } from "app/_types";
|
import type { PageProps } from "app/_types";
|
||||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||||
|
import { cookies, headers } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
|
|
||||||
|
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||||
|
|
||||||
import { validStatuses } from "~/bookings/lib/validStatuses";
|
import { validStatuses } from "~/bookings/lib/validStatuses";
|
||||||
import BookingsList from "~/bookings/views/bookings-listing-view";
|
import BookingsList from "~/bookings/views/bookings-listing-view";
|
||||||
|
|
||||||
@@ -26,10 +31,11 @@ const Page = async ({ params }: PageProps) => {
|
|||||||
redirect("/bookings/upcoming");
|
redirect("/bookings/upcoming");
|
||||||
}
|
}
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
|
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShellMainAppDir heading={t("bookings")} subtitle={t("bookings_description")}>
|
<ShellMainAppDir heading={t("bookings")} subtitle={t("bookings_description")}>
|
||||||
<BookingsList status={parsed.data.status} />
|
<BookingsList status={parsed.data.status} userId={session?.user?.id} />
|
||||||
</ShellMainAppDir>
|
</ShellMainAppDir>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
|
||||||
|
|
||||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
|
||||||
import { Button } from "@calcom/ui/components/button";
|
|
||||||
|
|
||||||
export const generateMetadata = () =>
|
|
||||||
_generateMetadata(
|
|
||||||
(t) => `${t("access_denied")} | ${APP_NAME}`,
|
|
||||||
() => "",
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
"/403"
|
|
||||||
);
|
|
||||||
|
|
||||||
async function Error403() {
|
|
||||||
const t = await getTranslate();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-subtle flex h-screen">
|
|
||||||
<div className="rtl: bg-default m-auto rounded-md p-10 text-right ltr:text-left">
|
|
||||||
<h1 className="font-cal text-emphasis text-6xl">403</h1>
|
|
||||||
<h2 className="text-emphasis mt-6 text-2xl font-medium">{t("dont_have_access_this_page")}</h2>
|
|
||||||
<p className="text-default mb-6 mt-4 max-w-2xl text-sm">
|
|
||||||
{t("you_need_admin_or_owner_privileges_to_access")}
|
|
||||||
</p>
|
|
||||||
<Button href={WEBAPP_URL}>{t("go_back_home")}</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Error403;
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { Button } from "@calcom/ui/components/button";
|
|
||||||
import { showToast } from "@calcom/ui/components/toast";
|
|
||||||
|
|
||||||
export default function CopyButton({ error }: { error: string }) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
color="secondary"
|
|
||||||
className="mt-2 border-0 font-sans font-normal hover:bg-gray-300"
|
|
||||||
StartIcon="copy"
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(error);
|
|
||||||
showToast("Link copied!", "success");
|
|
||||||
}}>
|
|
||||||
{t("copy")}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
|
||||||
|
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
|
||||||
import { Button } from "@calcom/ui/components/button";
|
|
||||||
|
|
||||||
import CopyButton from "./copy-button";
|
|
||||||
|
|
||||||
export const generateMetadata = () =>
|
|
||||||
_generateMetadata(
|
|
||||||
(t) => `${t("something_unexpected_occurred")} | ${APP_NAME}`,
|
|
||||||
() => "",
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
"/500"
|
|
||||||
);
|
|
||||||
|
|
||||||
async function Error500({ searchParams }: { searchParams: { error?: string } }) {
|
|
||||||
const t = await getTranslate();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-subtle flex h-screen">
|
|
||||||
<div className="rtl: bg-default m-auto rounded-md p-10 text-right ltr:text-left">
|
|
||||||
<h1 className="font-cal text-emphasis text-6xl">500</h1>
|
|
||||||
<h2 className="text-emphasis mt-6 text-2xl font-medium">{t("500_error_message")}</h2>
|
|
||||||
<p className="text-default mb-6 mt-4 max-w-2xl text-sm">{t("something_went_wrong_on_our_end")}</p>
|
|
||||||
{searchParams?.error && (
|
|
||||||
<div className="mb-8 flex flex-col">
|
|
||||||
<p className="text-default mb-4 max-w-2xl text-sm">
|
|
||||||
{t("please_provide_following_text_to_suppport")}:
|
|
||||||
</p>
|
|
||||||
<pre className="bg-emphasis text-emphasis w-full max-w-2xl whitespace-normal break-words rounded-md p-4">
|
|
||||||
{searchParams.error}
|
|
||||||
<br />
|
|
||||||
<CopyButton error={searchParams.error} />
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Button href="mailto:support@cal.com">{t("contact_support")}</Button>
|
|
||||||
<Button color="secondary" href="javascript:history.back()" className="ml-2">
|
|
||||||
{t("go_back")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Error500;
|
|
||||||
@@ -80,6 +80,48 @@ export const _generateMetadata = async (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const _generateMetadataForStaticPage = (
|
||||||
|
title: string,
|
||||||
|
description: string,
|
||||||
|
hideBranding?: boolean,
|
||||||
|
origin?: string,
|
||||||
|
pathname?: string
|
||||||
|
) => {
|
||||||
|
const _pathname = pathname ?? "";
|
||||||
|
const canonical = buildCanonical({ path: _pathname, origin: origin ?? CAL_URL });
|
||||||
|
const titleSuffix = `| ${APP_NAME}`;
|
||||||
|
const displayedTitle = title.includes(titleSuffix) || hideBranding ? title : `${title} ${titleSuffix}`;
|
||||||
|
const metadataBase = new URL(IS_CALCOM ? getCalcomUrl() : WEBAPP_URL);
|
||||||
|
|
||||||
|
const metadata = {
|
||||||
|
title: title.length === 0 ? APP_NAME : displayedTitle,
|
||||||
|
description,
|
||||||
|
alternates: { canonical },
|
||||||
|
openGraph: {
|
||||||
|
description: truncateOnWord(description, 158),
|
||||||
|
url: canonical,
|
||||||
|
type: "website",
|
||||||
|
siteName: APP_NAME,
|
||||||
|
title: displayedTitle,
|
||||||
|
},
|
||||||
|
metadataBase,
|
||||||
|
};
|
||||||
|
const image =
|
||||||
|
SEO_IMG_OGIMG +
|
||||||
|
constructGenericImage({
|
||||||
|
title: metadata.title,
|
||||||
|
description: metadata.description,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...metadata,
|
||||||
|
openGraph: {
|
||||||
|
...metadata.openGraph,
|
||||||
|
images: [image],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const generateMeetingMetadata = async (
|
export const generateMeetingMetadata = async (
|
||||||
meeting: MeetingImageProps,
|
meeting: MeetingImageProps,
|
||||||
getTitle: (t: TFunction<string, undefined>) => string,
|
getTitle: (t: TFunction<string, undefined>) => string,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||||
import { parseRequestData } from "app/api/parseRequestData";
|
import { parseRequestData } from "app/api/parseRequestData";
|
||||||
|
import * as jose from "jose";
|
||||||
import type { NextRequest } from "next/server";
|
import type { NextRequest } from "next/server";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
import * as dummy from "openid-client";
|
||||||
import { uuid } from "short-uuid";
|
import { uuid } from "short-uuid";
|
||||||
|
|
||||||
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||||
@@ -9,6 +11,10 @@ import type { OAuthTokenReq } from "@calcom/features/ee/sso/lib/jackson";
|
|||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
|
|
||||||
async function handler(req: NextRequest) {
|
async function handler(req: NextRequest) {
|
||||||
|
// Need these imports to fix import errors with jackson
|
||||||
|
// https://github.com/ory/polis/blob/main/pages/api/import-hack.ts
|
||||||
|
const unused = dummy; // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
const unused2 = jose; // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
const { oauthController } = await jackson();
|
const { oauthController } = await jackson();
|
||||||
const log = logger.getSubLogger({ prefix: ["[SAML token]"] });
|
const log = logger.getSubLogger({ prefix: ["[SAML token]"] });
|
||||||
|
|
||||||
|
|||||||
@@ -193,25 +193,27 @@ async function getHandler(request: NextRequest) {
|
|||||||
const response = await fetch(filteredLogo);
|
const response = await fetch(filteredLogo);
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
let buffer: Buffer = Buffer.from(arrayBuffer);
|
let buffer: Buffer = Buffer.from(arrayBuffer);
|
||||||
|
let contentType = response.headers.get("content-type") || "image/png";
|
||||||
|
|
||||||
// If we need to resize the team logos (via Next.js' built-in image processing)
|
// Resize the team logos if needed
|
||||||
if (teamLogos[logoDefinition.source] && logoDefinition.w) {
|
if (teamLogos[logoDefinition.source] && logoDefinition.w) {
|
||||||
const { detectContentType, optimizeImage } = await import("next/dist/server/image-optimizer");
|
const { resizeImage } = await import("@calcom/lib/server/imageUtils");
|
||||||
|
const { buffer: outBuffer, contentType: outContentType } = await resizeImage({
|
||||||
buffer = await optimizeImage({
|
|
||||||
buffer,
|
buffer,
|
||||||
contentType: (await detectContentType(buffer)) ?? "image/jpeg",
|
|
||||||
quality: 100,
|
|
||||||
width: logoDefinition.w,
|
width: logoDefinition.w,
|
||||||
height: logoDefinition.h, // optional
|
height: logoDefinition.h,
|
||||||
|
quality: 100,
|
||||||
|
contentType,
|
||||||
});
|
});
|
||||||
|
buffer = outBuffer;
|
||||||
|
contentType = outContentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new response with the image buffer
|
// Create a new response with the image buffer
|
||||||
const imageResponse = new NextResponse(buffer as BodyInit);
|
const imageResponse = new NextResponse(buffer as BodyInit);
|
||||||
|
|
||||||
// Set the appropriate headers
|
// Set the appropriate headers
|
||||||
imageResponse.headers.set("Content-Type", response.headers.get("content-type") || "image/png");
|
imageResponse.headers.set("Content-Type", contentType);
|
||||||
imageResponse.headers.set("Cache-Control", "s-maxage=86400, stale-while-revalidate=60");
|
imageResponse.headers.set("Cache-Control", "s-maxage=86400, stale-while-revalidate=60");
|
||||||
|
|
||||||
return imageResponse;
|
return imageResponse;
|
||||||
|
|||||||
+14
-13
@@ -1,4 +1,5 @@
|
|||||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
import { _generateMetadataForStaticPage } from "app/_utils";
|
||||||
|
import type { Metadata } from "next";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import localFont from "next/font/local";
|
import localFont from "next/font/local";
|
||||||
|
|
||||||
@@ -8,6 +9,15 @@ import type { IconName } from "@calcom/ui/components/icon";
|
|||||||
import { lucideIconList } from "../../../../packages/ui/components/icon/icon-list.mjs";
|
import { lucideIconList } from "../../../../packages/ui/components/icon/icon-list.mjs";
|
||||||
import { IconGrid } from "./IconGrid";
|
import { IconGrid } from "./IconGrid";
|
||||||
|
|
||||||
|
export const dynamic = "force-static";
|
||||||
|
export const metadata: Metadata = _generateMetadataForStaticPage(
|
||||||
|
"Icons Showcase",
|
||||||
|
"",
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
"/icons"
|
||||||
|
);
|
||||||
|
|
||||||
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
|
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
|
||||||
const calFont = localFont({
|
const calFont = localFont({
|
||||||
src: "../../fonts/CalSans-SemiBold.woff2",
|
src: "../../fonts/CalSans-SemiBold.woff2",
|
||||||
@@ -16,25 +26,16 @@ const calFont = localFont({
|
|||||||
display: "swap",
|
display: "swap",
|
||||||
weight: "600",
|
weight: "600",
|
||||||
});
|
});
|
||||||
export const generateMetadata = async () => {
|
|
||||||
return await _generateMetadata(
|
export default function IconsPage() {
|
||||||
(t) => t("icon_showcase"),
|
|
||||||
() => "",
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
"/icons"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default async function IconsPage() {
|
|
||||||
const icons = Array.from(lucideIconList).sort() as IconName[];
|
const icons = Array.from(lucideIconList).sort() as IconName[];
|
||||||
const t = await getTranslate();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${interFont.variable} ${calFont.variable}`}>
|
<div className={`${interFont.variable} ${calFont.variable}`}>
|
||||||
<div className="bg-subtle flex h-screen">
|
<div className="bg-subtle flex h-screen">
|
||||||
<IconSprites />
|
<IconSprites />
|
||||||
<div className="bg-default m-auto min-w-full rounded-md p-10 text-right ltr:text-left">
|
<div className="bg-default m-auto min-w-full rounded-md p-10 text-right ltr:text-left">
|
||||||
<h1 className="text-emphasis font-cal text-2xl font-medium">{t("icons_showcase")}</h1>
|
<h1 className="text-emphasis font-cal text-2xl font-medium">Icons Showcase</h1>
|
||||||
<IconGrid title="Regular Icons" icons={icons} />
|
<IconGrid title="Regular Icons" icons={icons} />
|
||||||
<IconGrid
|
<IconGrid
|
||||||
title="Filled Icons"
|
title="Filled Icons"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { TrpcProvider } from "app/_trpc/trpc-provider";
|
import { TrpcProvider } from "app/_trpc/trpc-provider";
|
||||||
import { SessionProvider } from "next-auth/react";
|
import { SessionProvider } from "next-auth/react";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
import CacheProvider from "react-inlinesvg/provider";
|
import CacheProvider from "react-inlinesvg/provider";
|
||||||
|
|
||||||
import { WebPushProvider } from "@calcom/features/notifications/WebPushContext";
|
import { WebPushProvider } from "@calcom/features/notifications/WebPushContext";
|
||||||
@@ -17,11 +18,13 @@ type ProvidersProps = {
|
|||||||
};
|
};
|
||||||
export function Providers({ isEmbed, children, nonce }: ProvidersProps) {
|
export function Providers({ isEmbed, children, nonce }: ProvidersProps) {
|
||||||
const isBookingPage = useIsBookingPage();
|
const isBookingPage = useIsBookingPage();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const isOnboardingPage = pathname?.startsWith("/getting-started");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
<TrpcProvider>
|
<TrpcProvider>
|
||||||
{!isBookingPage ? <PlainChat nonce={nonce} /> : null}
|
{!isBookingPage && !isOnboardingPage ? <PlainChat nonce={nonce} /> : null}
|
||||||
{!isEmbed && !isBookingPage && <NotificationSoundHandler />}
|
{!isEmbed && !isBookingPage && <NotificationSoundHandler />}
|
||||||
{/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */}
|
{/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */}
|
||||||
<CacheProvider>
|
<CacheProvider>
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ export const ReassignDialog = ({
|
|||||||
placeholder={t("search")}
|
placeholder={t("search")}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className="scroll-bar flex h-[150px] flex-col gap-0.5 overflow-y-scroll rounded-md border p-1">
|
<div className="scroll-bar mt-2 flex h-[150px] flex-col gap-0.5 overflow-y-scroll rounded-md border p-1">
|
||||||
{teamMemberOptions.map((member) => (
|
{teamMemberOptions.map((member) => (
|
||||||
<label
|
<label
|
||||||
key={member.value}
|
key={member.value}
|
||||||
|
|||||||
@@ -539,18 +539,11 @@ const NewRoutingManager = ({
|
|||||||
url: chosenEventUrls.eventBookingAbsoluteUrl,
|
url: chosenEventUrls.eventBookingAbsoluteUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("SETTING NEW VALUE", {
|
|
||||||
type: "reschedule_to_different_event_new_tab",
|
|
||||||
reschedulerWindow,
|
|
||||||
newBooking: null,
|
|
||||||
});
|
|
||||||
reroutingState.setValue({
|
reroutingState.setValue({
|
||||||
type: "reschedule_to_different_event_new_tab",
|
type: "reschedule_to_different_event_new_tab",
|
||||||
reschedulerWindow,
|
reschedulerWindow,
|
||||||
newBooking: null,
|
newBooking: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("VALUE NOW", JSON.stringify(reroutingState));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function reroutingPreview() {
|
function reroutingPreview() {
|
||||||
@@ -910,7 +903,7 @@ export const RerouteDialog = ({ isOpenDialog, setIsOpenDialog, booking }: Rerout
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
||||||
<DialogContent enableOverflow preventCloseOnOutsideClick>
|
<DialogContent preventCloseOnOutsideClick>
|
||||||
<DialogHeader title={t("reroute_booking")} subtitle={t("reroute_booking_description")} />
|
<DialogHeader title={t("reroute_booking")} subtitle={t("reroute_booking_description")} />
|
||||||
<RerouteDialogContentAndFooter
|
<RerouteDialogContentAndFooter
|
||||||
booking={teamEventTypeBooking}
|
booking={teamEventTypeBooking}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ const AppConnectionItem = (props: IAppConnectionItem) => {
|
|||||||
{t("this_app_requires_connected_account", {
|
{t("this_app_requires_connected_account", {
|
||||||
appName: title,
|
appName: title,
|
||||||
dependencyName: dependency.name,
|
dependencyName: dependency.name,
|
||||||
|
interpolation: { escapeValue: false },
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
|||||||
loading={isPageLoading}
|
loading={isPageLoading}
|
||||||
onClick={() => nextStep()}
|
onClick={() => nextStep()}
|
||||||
disabled={disabledNextButton}>
|
disabled={disabledNextButton}>
|
||||||
{firstCalendar ? `${t("continue")}` : `${t("next_step_text")}`}
|
{firstCalendar ? `${t("connect_your_video")}` : `${t("connect_calendar_first")}`}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
|
|||||||
disabled={!hasAnyInstalledVideoApps}
|
disabled={!hasAnyInstalledVideoApps}
|
||||||
loading={isPageLoading}
|
loading={isPageLoading}
|
||||||
onClick={() => nextStep()}>
|
onClick={() => nextStep()}>
|
||||||
{t("next_step_text")}
|
{t("set_availability")}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
|
|||||||
className="mt-2 w-full justify-center p-2 text-sm sm:mt-8"
|
className="mt-2 w-full justify-center p-2 text-sm sm:mt-8"
|
||||||
loading={availabilityForm.formState.isSubmitting}
|
loading={availabilityForm.formState.isSubmitting}
|
||||||
disabled={availabilityForm.formState.isSubmitting}>
|
disabled={availabilityForm.formState.isSubmitting}>
|
||||||
{t("next_step_text")}
|
{t("complete_profile")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ const UserProfile = ({ user }: UserProfileProps) => {
|
|||||||
EndIcon="arrow-right"
|
EndIcon="arrow-right"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="mt-8 w-full items-center justify-center">
|
className="mt-8 w-full items-center justify-center">
|
||||||
{t("finish")}
|
{t("finish_and_start")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
type="text"
|
type="text"
|
||||||
|
placeholder="John Doe"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
/>
|
/>
|
||||||
@@ -121,7 +122,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
className="mt-8 flex w-full flex-row justify-center"
|
className="mt-8 flex w-full flex-row justify-center"
|
||||||
loading={mutation.isPending}
|
loading={mutation.isPending}
|
||||||
disabled={mutation.isPending}>
|
disabled={mutation.isPending}>
|
||||||
{t("next_step_text")}
|
{t("connect_your_calendar")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ export function NotificationSoundHandler() {
|
|||||||
const response = await fetch("/ring.mp3");
|
const response = await fetch("/ring.mp3");
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
audioBufferRef.current = await audioContextRef.current.decodeAudioData(arrayBuffer);
|
audioBufferRef.current = await audioContextRef.current.decodeAudioData(arrayBuffer);
|
||||||
console.log("Audio file loaded and decoded");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -62,7 +61,6 @@ export function NotificationSoundHandler() {
|
|||||||
|
|
||||||
source.loop = true;
|
source.loop = true;
|
||||||
source.start(0);
|
source.start(0);
|
||||||
console.log("Sound started playing");
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (sourceRef.current === source) {
|
if (sourceRef.current === source) {
|
||||||
@@ -101,7 +99,7 @@ export function NotificationSoundHandler() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!("serviceWorker" in navigator)) {
|
if (!("serviceWorker" in navigator)) {
|
||||||
console.log("ServiceWorker not available");
|
console.warn("ServiceWorker not available");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
|
|||||||
<TextField
|
<TextField
|
||||||
ref={usernameRef}
|
ref={usernameRef}
|
||||||
name="username"
|
name="username"
|
||||||
|
placeholder="john"
|
||||||
value={inputUsernameValue}
|
value={inputUsernameValue}
|
||||||
autoComplete="none"
|
autoComplete="none"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN_CLIENT,
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN_CLIENT,
|
||||||
|
|
||||||
@@ -40,5 +41,10 @@ Sentry.init({
|
|||||||
return event;
|
return event;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
|
export function onRouterTransitionStart(url: string, navigationType: "push" | "replace" | "traverse") {
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
Sentry.captureRouterTransitionStart(url, navigationType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
|
import { type Instrumentation } from 'next'
|
||||||
|
|
||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
export async function register() {
|
export async function register() {
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "nodejs") {
|
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "nodejs") {
|
||||||
await import("./sentry.server.config");
|
await import("./sentry.server.config");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "edge") {
|
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "edge") {
|
||||||
await import("./sentry.edge.config");
|
await import("./sentry.edge.config");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const onRequestError = Sentry.captureRequestError;
|
export const onRequestError: Instrumentation.onRequestError = ( err,
|
||||||
|
request,
|
||||||
|
context) => {
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
Sentry.captureRequestError(err, request, context);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const getEventTypesFromDB = async (id: number) => {
|
|||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
email: true,
|
email: true,
|
||||||
timeZone: true,
|
timeZone: true,
|
||||||
|
isPlatformManaged: true,
|
||||||
};
|
};
|
||||||
const eventType = await prisma.eventType.findUnique({
|
const eventType = await prisma.eventType.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@@ -75,6 +76,7 @@ export const getEventTypesFromDB = async (id: number) => {
|
|||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
createdByOAuthClientId: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
workflows: {
|
workflows: {
|
||||||
|
|||||||
@@ -60,22 +60,6 @@ const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
|
|||||||
const reqWithEnrichedHeaders = enrichRequestWithHeaders({ req });
|
const reqWithEnrichedHeaders = enrichRequestWithHeaders({ req });
|
||||||
const requestHeaders = new Headers(reqWithEnrichedHeaders.headers);
|
const requestHeaders = new Headers(reqWithEnrichedHeaders.headers);
|
||||||
|
|
||||||
if (!url.pathname.startsWith("/api")) {
|
|
||||||
//
|
|
||||||
// NOTE: When tRPC hits an error a 500 is returned, when this is received
|
|
||||||
// by the application the user is automatically redirected to /auth/login.
|
|
||||||
//
|
|
||||||
// - For this reason our matchers are sufficient for an app-wide maintenance page.
|
|
||||||
//
|
|
||||||
// Check whether the maintenance page should be shown
|
|
||||||
const isInMaintenanceMode = await safeGet<boolean>("isInMaintenanceMode");
|
|
||||||
// If is in maintenance mode, point the url pathname to the maintenance page
|
|
||||||
if (isInMaintenanceMode) {
|
|
||||||
reqWithEnrichedHeaders.nextUrl.pathname = `/maintenance`;
|
|
||||||
return NextResponse.rewrite(reqWithEnrichedHeaders.nextUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const routingFormRewriteResponse = routingForms.handleRewrite(url);
|
const routingFormRewriteResponse = routingForms.handleRewrite(url);
|
||||||
if (routingFormRewriteResponse) {
|
if (routingFormRewriteResponse) {
|
||||||
return responseWithHeaders({ url, res: routingFormRewriteResponse, req: reqWithEnrichedHeaders });
|
return responseWithHeaders({ url, res: routingFormRewriteResponse, req: reqWithEnrichedHeaders });
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { APP_NAME } from "@calcom/lib/constants";
|
|||||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants/permissions";
|
import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants/permissions";
|
||||||
import { Icon } from "@calcom/ui/components/icon";
|
|
||||||
import { Avatar } from "@calcom/ui/components/avatar";
|
import { Avatar } from "@calcom/ui/components/avatar";
|
||||||
import { Button } from "@calcom/ui/components/button";
|
import { Button } from "@calcom/ui/components/button";
|
||||||
|
import { Icon } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { hasPermission } from "../../../../../packages/platform/utils/permissions";
|
import { hasPermission } from "../../../../../packages/platform/utils/permissions";
|
||||||
|
|
||||||
@@ -32,8 +32,6 @@ export default function Authorize() {
|
|||||||
permissions: 7,
|
permissions: 7,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("These are the search params:", queryString);
|
|
||||||
|
|
||||||
const permissions = Object.values(PERMISSIONS_GROUPED_MAP).map((value) => {
|
const permissions = Object.values(PERMISSIONS_GROUPED_MAP).map((value) => {
|
||||||
let permissionsMessage = "";
|
let permissionsMessage = "";
|
||||||
const hasReadPermission = hasPermission(client.permissions, value.read);
|
const hasReadPermission = hasPermission(client.permissions, value.read);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useReactTable, getCoreRowModel, getSortedRowModel, createColumnHelper } from "@tanstack/react-table";
|
import { useReactTable, getCoreRowModel, getSortedRowModel, createColumnHelper } from "@tanstack/react-table";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams, usePathname } from "next/navigation";
|
||||||
import { useMemo, useRef } from "react";
|
import { useMemo, useRef } from "react";
|
||||||
|
|
||||||
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
|
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
|
||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
ZMultiSelectFilterValue,
|
ZMultiSelectFilterValue,
|
||||||
ZDateRangeFilterValue,
|
ZDateRangeFilterValue,
|
||||||
ZTextFilterValue,
|
ZTextFilterValue,
|
||||||
|
type SystemFilterSegment,
|
||||||
} from "@calcom/features/data-table";
|
} from "@calcom/features/data-table";
|
||||||
import { useSegments } from "@calcom/features/data-table/hooks/useSegments";
|
import { useSegments } from "@calcom/features/data-table/hooks/useSegments";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -55,11 +56,45 @@ const descriptionByStatus: Record<BookingListingStatus, string> = {
|
|||||||
|
|
||||||
type BookingsProps = {
|
type BookingsProps = {
|
||||||
status: (typeof validStatuses)[number];
|
status: (typeof validStatuses)[number];
|
||||||
|
userId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function useSystemSegments(userId?: number) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
const systemSegments: SystemFilterSegment[] = useMemo(() => {
|
||||||
|
if (!userId) return [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "my_bookings",
|
||||||
|
name: t("my_bookings"),
|
||||||
|
type: "system",
|
||||||
|
activeFilters: [
|
||||||
|
{
|
||||||
|
f: "userId",
|
||||||
|
v: {
|
||||||
|
type: ColumnFilterType.MULTI_SELECT,
|
||||||
|
data: [userId],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
perPage: 10,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [userId, t]);
|
||||||
|
|
||||||
|
return systemSegments;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Bookings(props: BookingsProps) {
|
export default function Bookings(props: BookingsProps) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const systemSegments = useSystemSegments(props.userId);
|
||||||
return (
|
return (
|
||||||
<DataTableProvider useSegments={useSegments}>
|
<DataTableProvider
|
||||||
|
useSegments={useSegments}
|
||||||
|
systemSegments={systemSegments}
|
||||||
|
tableIdentifier={pathname || undefined}>
|
||||||
<BookingsContent {...props} />
|
<BookingsContent {...props} />
|
||||||
</DataTableProvider>
|
</DataTableProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -228,11 +228,15 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
? { ...previousBooking, rescheduledBy: bookingInfo.user?.name }
|
? { ...previousBooking, rescheduledBy: bookingInfo.user?.name }
|
||||||
: previousBooking;
|
: previousBooking;
|
||||||
|
|
||||||
|
const isPlatformBooking = eventType.users[0]?.isPlatformManaged || eventType.team?.createdByOAuthClientId;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
orgSlug: currentOrgDomain,
|
orgSlug: currentOrgDomain,
|
||||||
themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username,
|
themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username,
|
||||||
hideBranding: await shouldHideBrandingForEvent({
|
hideBranding: isPlatformBooking
|
||||||
|
? true
|
||||||
|
: await shouldHideBrandingForEvent({
|
||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
team: eventType.team,
|
team: eventType.team,
|
||||||
owner: eventType.users[0] ?? null,
|
owner: eventType.users[0] ?? null,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const getStepsAndHeadersForUser = (t: TFunction) => {
|
|||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
title: t("welcome_to_cal_header", { appName: APP_NAME }),
|
title: t("welcome_to_cal_header", { appName: APP_NAME }),
|
||||||
subtitle: [t("we_just_need_basic_info"), t("edit_form_later_subtitle")],
|
subtitle: [t("we_just_need_basic_info")],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("connect_your_calendar"),
|
title: t("connect_your_calendar"),
|
||||||
@@ -57,8 +57,9 @@ const getStepsAndHeadersForUser = (t: TFunction) => {
|
|||||||
{
|
{
|
||||||
title: t("set_availability"),
|
title: t("set_availability"),
|
||||||
subtitle: [
|
subtitle: [
|
||||||
t("set_availability_getting_started_subtitle_1"),
|
`${t("set_availability_getting_started_subtitle_1")} ${t(
|
||||||
t("set_availability_getting_started_subtitle_2"),
|
"set_availability_getting_started_subtitle_2"
|
||||||
|
)}`,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -138,7 +139,8 @@ const OnboardingPage = (props: PageProps) => {
|
|||||||
"dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen [--cal-brand:#111827] dark:[--cal-brand:#FFFFFF]",
|
"dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen [--cal-brand:#111827] dark:[--cal-brand:#FFFFFF]",
|
||||||
"[--cal-brand-emphasis:#101010] dark:[--cal-brand-emphasis:#e1e1e1]",
|
"[--cal-brand-emphasis:#101010] dark:[--cal-brand-emphasis:#e1e1e1]",
|
||||||
"[--cal-brand-subtle:#9CA3AF]",
|
"[--cal-brand-subtle:#9CA3AF]",
|
||||||
"[--cal-brand-text:#FFFFFF] dark:[--cal-brand-text:#000000]"
|
"[--cal-brand-text:#FFFFFF] dark:[--cal-brand-text:#000000]",
|
||||||
|
"[--cal-brand-accent:#FFFFFF] dark:[--cal-brand-accent:#000000]"
|
||||||
)}
|
)}
|
||||||
data-testid="onboarding"
|
data-testid="onboarding"
|
||||||
key={pathname}>
|
key={pathname}>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
ColumnFilterType,
|
ColumnFilterType,
|
||||||
type FilterableColumn,
|
type FilterableColumn,
|
||||||
} from "@calcom/features/data-table";
|
} from "@calcom/features/data-table";
|
||||||
|
import { useSegments } from "@calcom/features/data-table/hooks/useSegments";
|
||||||
import {
|
import {
|
||||||
AverageEventDurationChart,
|
AverageEventDurationChart,
|
||||||
BookingKPICards,
|
BookingKPICards,
|
||||||
@@ -18,6 +19,8 @@ import {
|
|||||||
LowestRatedMembersTable,
|
LowestRatedMembersTable,
|
||||||
MostBookedTeamMembersTable,
|
MostBookedTeamMembersTable,
|
||||||
MostCancelledBookingsTables,
|
MostCancelledBookingsTables,
|
||||||
|
MostCompletedTeamMembersTable,
|
||||||
|
LeastCompletedTeamMembersTable,
|
||||||
PopularEventsTable,
|
PopularEventsTable,
|
||||||
RecentFeedbackTable,
|
RecentFeedbackTable,
|
||||||
TimezoneBadge,
|
TimezoneBadge,
|
||||||
@@ -31,7 +34,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
|
|
||||||
export default function InsightsPage({ timeZone }: { timeZone: string }) {
|
export default function InsightsPage({ timeZone }: { timeZone: string }) {
|
||||||
return (
|
return (
|
||||||
<DataTableProvider timeZone={timeZone}>
|
<DataTableProvider useSegments={useSegments} timeZone={timeZone}>
|
||||||
<InsightsOrgTeamsProvider>
|
<InsightsOrgTeamsProvider>
|
||||||
<InsightsPageContent />
|
<InsightsPageContent />
|
||||||
</InsightsOrgTeamsProvider>
|
</InsightsOrgTeamsProvider>
|
||||||
@@ -81,20 +84,23 @@ function InsightsPageContent() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
|
||||||
<div className="sm:col-span-2">
|
|
||||||
<PopularEventsTable />
|
|
||||||
</div>
|
|
||||||
<MostBookedTeamMembersTable />
|
<MostBookedTeamMembersTable />
|
||||||
<LeastBookedTeamMembersTable />
|
<LeastBookedTeamMembersTable />
|
||||||
|
<MostCompletedTeamMembersTable />
|
||||||
|
<LeastCompletedTeamMembersTable />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
|
||||||
<MostCancelledBookingsTables />
|
<MostCancelledBookingsTables />
|
||||||
<HighestNoShowHostTable />
|
<HighestNoShowHostTable />
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<PopularEventsTable />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
|
||||||
<HighestRatedMembersTable />
|
<HighestRatedMembersTable />
|
||||||
<LowestRatedMembersTable />
|
<LowestRatedMembersTable />
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
|
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<RecentFeedbackTable />
|
<RecentFeedbackTable />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ export default function Signup({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (redirectUrl) {
|
if (redirectUrl) {
|
||||||
|
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
|
||||||
localStorage.setItem("onBoardingRedirect", redirectUrl);
|
localStorage.setItem("onBoardingRedirect", redirectUrl);
|
||||||
}
|
}
|
||||||
}, [redirectUrl]);
|
}, [redirectUrl]);
|
||||||
@@ -216,7 +217,6 @@ export default function Signup({
|
|||||||
if (err.checkoutSessionId) {
|
if (err.checkoutSessionId) {
|
||||||
const stripe = await getStripe();
|
const stripe = await getStripe();
|
||||||
if (stripe) {
|
if (stripe) {
|
||||||
console.log("Redirecting to stripe checkout");
|
|
||||||
const { error } = await stripe.redirectToCheckout({
|
const { error } = await stripe.redirectToCheckout({
|
||||||
sessionId: err.checkoutSessionId,
|
sessionId: err.checkoutSessionId,
|
||||||
});
|
});
|
||||||
@@ -470,6 +470,7 @@ export default function Signup({
|
|||||||
showToast("error", t("username_required"));
|
showToast("error", t("username_required"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
|
||||||
localStorage.setItem("username", username);
|
localStorage.setItem("username", username);
|
||||||
const sp = new URLSearchParams();
|
const sp = new URLSearchParams();
|
||||||
// @NOTE: don't remove username query param as it's required right now for stripe payment page
|
// @NOTE: don't remove username query param as it's required right now for stripe payment page
|
||||||
@@ -539,6 +540,7 @@ export default function Signup({
|
|||||||
if (prepopulateFormValues?.username) {
|
if (prepopulateFormValues?.username) {
|
||||||
// If username is present we save it in query params to check for premium
|
// If username is present we save it in query params to check for premium
|
||||||
searchQueryParams.set("username", prepopulateFormValues.username);
|
searchQueryParams.set("username", prepopulateFormValues.username);
|
||||||
|
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
|
||||||
localStorage.setItem("username", prepopulateFormValues.username);
|
localStorage.setItem("username", prepopulateFormValues.username);
|
||||||
}
|
}
|
||||||
if (token) {
|
if (token) {
|
||||||
|
|||||||
+25
-3
@@ -170,7 +170,18 @@ const orgDomainMatcherConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = (phase) => {
|
||||||
|
if (isOrganizationsEnabled) {
|
||||||
|
// We want to log the phase here because it is important that the rewrite is added during the build phase(phase=phase-production-build)
|
||||||
|
console.log(
|
||||||
|
`[Phase: ${phase}] Adding rewrite config for organizations - orgHostPath: ${nextJsOrgRewriteConfig.orgHostPath}, orgSlug: ${nextJsOrgRewriteConfig.orgSlug}, disableRootPathRewrite: ${nextJsOrgRewriteConfig.disableRootPathRewrite}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`[Phase: ${phase}] Skipping rewrite config for organizations because ORGANIZATIONS_ENABLED is not set`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
output: process.env.BUILD_STANDALONE === "true" ? "standalone" : undefined,
|
output: process.env.BUILD_STANDALONE === "true" ? "standalone" : undefined,
|
||||||
serverExternalPackages: [
|
serverExternalPackages: [
|
||||||
"deasync",
|
"deasync",
|
||||||
@@ -185,6 +196,8 @@ const nextConfig = {
|
|||||||
experimental: {
|
experimental: {
|
||||||
// externalize server-side node_modules with size > 1mb, to improve dev mode performance/RAM usage
|
// externalize server-side node_modules with size > 1mb, to improve dev mode performance/RAM usage
|
||||||
optimizePackageImports: ["@calcom/ui"],
|
optimizePackageImports: ["@calcom/ui"],
|
||||||
|
webpackMemoryOptimizations: true,
|
||||||
|
webpackBuildWorker: true,
|
||||||
},
|
},
|
||||||
productionBrowserSourceMaps: true,
|
productionBrowserSourceMaps: true,
|
||||||
/* We already do type check on GH actions */
|
/* We already do type check on GH actions */
|
||||||
@@ -220,7 +233,15 @@ const nextConfig = {
|
|||||||
images: {
|
images: {
|
||||||
unoptimized: true,
|
unoptimized: true,
|
||||||
},
|
},
|
||||||
webpack: (config, { webpack, buildId, isServer }) => {
|
webpack: (config, { webpack, buildId, isServer, dev }) => {
|
||||||
|
if (!dev) {
|
||||||
|
if (config.cache) {
|
||||||
|
config.cache = Object.freeze({
|
||||||
|
type: "memory",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isServer) {
|
if (isServer) {
|
||||||
// Module not found fix @see https://github.com/boxyhq/jackson/issues/1535#issuecomment-1704381612
|
// Module not found fix @see https://github.com/boxyhq/jackson/issues/1535#issuecomment-1704381612
|
||||||
config.plugins.push(
|
config.plugins.push(
|
||||||
@@ -678,5 +699,6 @@ const nextConfig = {
|
|||||||
return redirects;
|
return redirects;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
module.exports = (phase) => plugins.reduce((acc, next) => next(acc), nextConfig(phase));
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "5.6.2",
|
"version": "5.6.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"yarn": "3.4.1"
|
"yarn": "3.4.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@boxyhq/saml-jackson": "1.42.0",
|
"@boxyhq/saml-jackson": "1.52.2",
|
||||||
"@calcom/app-store": "*",
|
"@calcom/app-store": "*",
|
||||||
"@calcom/app-store-cli": "*",
|
"@calcom/app-store-cli": "*",
|
||||||
"@calcom/dayjs": "*",
|
"@calcom/dayjs": "*",
|
||||||
@@ -77,7 +77,6 @@
|
|||||||
"@tanstack/react-query": "^5.17.15",
|
"@tanstack/react-query": "^5.17.15",
|
||||||
"@team-plain/typescript-sdk": "^5.9.0",
|
"@team-plain/typescript-sdk": "^5.9.0",
|
||||||
"@types/turndown": "^5.0.1",
|
"@types/turndown": "^5.0.1",
|
||||||
"@unkey/ratelimit": "^0.1.1",
|
|
||||||
"@upstash/redis": "^1.35.2",
|
"@upstash/redis": "^1.35.2",
|
||||||
"@vercel/edge-config": "^0.1.1",
|
"@vercel/edge-config": "^0.1.1",
|
||||||
"@vercel/edge-functions-ui": "^0.2.1",
|
"@vercel/edge-functions-ui": "^0.2.1",
|
||||||
@@ -115,6 +114,7 @@
|
|||||||
"next-themes": "^0.2.0",
|
"next-themes": "^0.2.0",
|
||||||
"nodemailer": "^6.7.8",
|
"nodemailer": "^6.7.8",
|
||||||
"nuqs": "^1.20.0",
|
"nuqs": "^1.20.0",
|
||||||
|
"openid-client": "6.5.0",
|
||||||
"otplib": "^12.0.1",
|
"otplib": "^12.0.1",
|
||||||
"posthog-js": "^1.164.1",
|
"posthog-js": "^1.164.1",
|
||||||
"posthog-node": "^4.2.0",
|
"posthog-node": "^4.2.0",
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { wrapApiHandlerWithSentry } from "@sentry/nextjs";
|
|
||||||
|
|
||||||
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
|
|
||||||
import { highPerfRouter } from "@calcom/trpc/server/routers/viewer/highPerf/_router";
|
|
||||||
|
|
||||||
export default wrapApiHandlerWithSentry(createNextApiHandler(highPerfRouter), "/api/trpc/highPerf/[trpc]");
|
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { expect } from "@playwright/test";
|
||||||
|
|
||||||
|
import { createTeamEventType } from "./fixtures/users";
|
||||||
|
import { test } from "./lib/fixtures";
|
||||||
|
|
||||||
|
test.describe("Duplicate API Calls Prevention", () => {
|
||||||
|
test.afterEach(({ users }) => users.deleteAll());
|
||||||
|
|
||||||
|
test("should detect when schedule endpoints are called multiple times for individual user events", async ({
|
||||||
|
page,
|
||||||
|
users,
|
||||||
|
}) => {
|
||||||
|
const user = await users.create();
|
||||||
|
const eventType = user.eventTypes.find((e) => e.slug === "30-min");
|
||||||
|
|
||||||
|
const trpcCalls: string[] = [];
|
||||||
|
const apiV2Calls: string[] = [];
|
||||||
|
|
||||||
|
// Intercept tRPC getSchedule calls - pattern matches trpc.viewer.slots.getSchedule.useQuery()
|
||||||
|
await page.route("**/api/trpc/viewer.slots.getSchedule**", async (route) => {
|
||||||
|
trpcCalls.push(route.request().url());
|
||||||
|
await route.continue();
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route("**/api/v2/slots/available**", async (route) => {
|
||||||
|
apiV2Calls.push(route.request().url());
|
||||||
|
await route.continue();
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`/${user.username}/${eventType?.slug}`);
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
const totalCalls = trpcCalls.length + apiV2Calls.length;
|
||||||
|
|
||||||
|
if (totalCalls === 0) {
|
||||||
|
console.log("No API calls detected - environment may have issues preventing page load");
|
||||||
|
expect(totalCalls).toBeGreaterThanOrEqual(0);
|
||||||
|
} else {
|
||||||
|
expect(totalCalls).toBeLessThanOrEqual(1);
|
||||||
|
expect(trpcCalls.length).toBeLessThanOrEqual(1);
|
||||||
|
expect(apiV2Calls.length).toBeLessThanOrEqual(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should detect when schedule endpoints are called multiple times for team events", async ({
|
||||||
|
page,
|
||||||
|
users,
|
||||||
|
}) => {
|
||||||
|
const teamOwner = await users.create(
|
||||||
|
{ name: "Team Owner" },
|
||||||
|
{
|
||||||
|
hasTeam: true,
|
||||||
|
teammates: [{ name: "teammate-1" }],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { team } = await teamOwner.getFirstTeamMembership();
|
||||||
|
const teamEvent = await createTeamEventType(
|
||||||
|
{ id: teamOwner.id },
|
||||||
|
{ id: team.id },
|
||||||
|
{ teamEventSlug: "team-event-test", teamEventTitle: "Team Event Test" }
|
||||||
|
);
|
||||||
|
|
||||||
|
const trpcCalls: string[] = [];
|
||||||
|
const apiV2Calls: string[] = [];
|
||||||
|
|
||||||
|
await page.route("**/api/trpc/viewer.slots.getSchedule**", async (route) => {
|
||||||
|
trpcCalls.push(route.request().url());
|
||||||
|
await route.continue();
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route("**/api/v2/slots/available**", async (route) => {
|
||||||
|
apiV2Calls.push(route.request().url());
|
||||||
|
await route.continue();
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`/team/${team.slug}/${teamEvent.slug}`);
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
const totalCalls = trpcCalls.length + apiV2Calls.length;
|
||||||
|
|
||||||
|
if (totalCalls === 0) {
|
||||||
|
console.log("No API calls detected - environment may have issues preventing page load");
|
||||||
|
expect(totalCalls).toBeGreaterThanOrEqual(0);
|
||||||
|
} else {
|
||||||
|
expect(totalCalls).toBeLessThanOrEqual(1);
|
||||||
|
expect(trpcCalls.length).toBeLessThanOrEqual(1);
|
||||||
|
expect(apiV2Calls.length).toBeLessThanOrEqual(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should detect when schedule endpoints are called multiple times for organization team events", async ({
|
||||||
|
page,
|
||||||
|
users,
|
||||||
|
}) => {
|
||||||
|
const orgOwner = await users.create(
|
||||||
|
{ name: "Org Owner" },
|
||||||
|
{
|
||||||
|
hasTeam: true,
|
||||||
|
teammates: [{ name: "teammate-1" }],
|
||||||
|
isOrg: true,
|
||||||
|
isOrgVerified: true,
|
||||||
|
hasSubteam: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { team } = await orgOwner.getFirstTeamMembership();
|
||||||
|
const { team: org } = await orgOwner.getOrgMembership();
|
||||||
|
const teamEvent = await createTeamEventType(
|
||||||
|
{ id: orgOwner.id },
|
||||||
|
{ id: team.id },
|
||||||
|
{ teamEventSlug: "org-team-event", teamEventTitle: "Org Team Event" }
|
||||||
|
);
|
||||||
|
|
||||||
|
const trpcCalls: string[] = [];
|
||||||
|
const apiV2Calls: string[] = [];
|
||||||
|
|
||||||
|
await page.route("**/api/trpc/viewer.slots.getSchedule**", async (route) => {
|
||||||
|
trpcCalls.push(route.request().url());
|
||||||
|
await route.continue();
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route("**/api/v2/slots/available**", async (route) => {
|
||||||
|
apiV2Calls.push(route.request().url());
|
||||||
|
await route.continue();
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`/org/${org.slug}/${team.slug}/${teamEvent.slug}`);
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
const totalCalls = trpcCalls.length + apiV2Calls.length;
|
||||||
|
|
||||||
|
if (totalCalls === 0) {
|
||||||
|
console.log("No API calls detected - environment may have issues preventing page load");
|
||||||
|
expect(totalCalls).toBeGreaterThanOrEqual(0);
|
||||||
|
} else {
|
||||||
|
expect(totalCalls).toBeLessThanOrEqual(1);
|
||||||
|
expect(trpcCalls.length).toBeLessThanOrEqual(1);
|
||||||
|
expect(apiV2Calls.length).toBeLessThanOrEqual(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -109,10 +109,7 @@ test("multiple duration selection updates event length correctly", async ({ page
|
|||||||
await page.waitForSelector('[data-testid="event-types"]');
|
await page.waitForSelector('[data-testid="event-types"]');
|
||||||
await page.click(`text=Multiple duration`);
|
await page.click(`text=Multiple duration`);
|
||||||
await page.waitForSelector('[data-testid="event-title"]');
|
await page.waitForSelector('[data-testid="event-title"]');
|
||||||
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
|
await expect(page.getByTestId("vertical-tab-basics")).toHaveAttribute("aria-current", "page");
|
||||||
"aria-current",
|
|
||||||
"page"
|
|
||||||
);
|
|
||||||
await page.getByTestId("vertical-tab-event_advanced_tab_title").click();
|
await page.getByTestId("vertical-tab-event_advanced_tab_title").click();
|
||||||
await page.fill('[name="eventName"]', "{Event duration} event btwn {Organiser} {Scheduler}");
|
await page.fill('[name="eventName"]', "{Event duration} event btwn {Organiser} {Scheduler}");
|
||||||
await page.locator('[data-testid="update-eventtype"]').click();
|
await page.locator('[data-testid="update-eventtype"]').click();
|
||||||
|
|||||||
@@ -77,10 +77,7 @@ test.describe("Event Types tests", () => {
|
|||||||
|
|
||||||
// fix the race condition
|
// fix the race condition
|
||||||
await page.waitForSelector('[data-testid="event-title"]');
|
await page.waitForSelector('[data-testid="event-title"]');
|
||||||
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
|
await expect(page.getByTestId("vertical-tab-basics")).toHaveAttribute("aria-current", "page");
|
||||||
"aria-current",
|
|
||||||
"page"
|
|
||||||
);
|
|
||||||
|
|
||||||
await page.click("[data-testid=vertical-tab-recurring]");
|
await page.click("[data-testid=vertical-tab-recurring]");
|
||||||
await expect(page.locator("[data-testid=recurring-event-collapsible]")).toBeHidden();
|
await expect(page.locator("[data-testid=recurring-event-collapsible]")).toBeHidden();
|
||||||
@@ -447,6 +444,37 @@ test.describe("Event Types tests", () => {
|
|||||||
await expect(currentTimezone).toHaveClass(/cursor-not-allowed/);
|
await expect(currentTimezone).toHaveClass(/cursor-not-allowed/);
|
||||||
await expect(page.getByText("New York")).toBeVisible();
|
await expect(page.getByText("New York")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
test("should create recurring event and successfully book multiple occurrences", async ({ page }) => {
|
||||||
|
const nonce = randomString(3);
|
||||||
|
const eventTitle = `Recurring Event Test ${nonce}`;
|
||||||
|
|
||||||
|
await createNewUserEventType(page, { eventTitle });
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="event-title"]');
|
||||||
|
await expect(page.getByTestId("vertical-tab-basics")).toHaveAttribute("aria-current", "page");
|
||||||
|
await page.click("[data-testid=vertical-tab-recurring]");
|
||||||
|
await expect(page.locator("[data-testid=recurring-event-collapsible]")).toBeHidden();
|
||||||
|
await page.click("[data-testid=recurring-event-check]");
|
||||||
|
await expect(page.locator("[data-testid=recurring-event-collapsible]")).toBeVisible();
|
||||||
|
|
||||||
|
await page.locator("[data-testid=recurring-event-collapsible] input[type=number]").nth(1).fill("3");
|
||||||
|
|
||||||
|
await saveEventType(page);
|
||||||
|
|
||||||
|
await gotoBookingPage(page);
|
||||||
|
|
||||||
|
await expect(page.locator("[data-testid=occurrence-input]")).toHaveValue("3");
|
||||||
|
|
||||||
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||||
|
|
||||||
|
await expect(page.locator("[data-testid=recurring-dates]")).toBeVisible();
|
||||||
|
|
||||||
|
await bookTimeSlot(page, { isRecurringEvent: true });
|
||||||
|
|
||||||
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||||
|
|
||||||
|
await expect(page.locator("text=3 occurrences")).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("Interface Language Tests", () => {
|
test.describe("Interface Language Tests", () => {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export async function applySelectFilter(page: Page, columnId: string, value: str
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function selectOptionValue(page: Page, columnId: string, value: string) {
|
export async function selectOptionValue(page: Page, columnId: string, value: string) {
|
||||||
|
await page.getByTestId(`select-filter-options-${columnId}`).getByRole("option", { name: value }).waitFor();
|
||||||
await page.getByTestId(`select-filter-options-${columnId}`).getByRole("option", { name: value }).click();
|
await page.getByTestId(`select-filter-options-${columnId}`).getByRole("option", { name: value }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,3 +177,71 @@ export async function listSegments(page: Page): Promise<string[]> {
|
|||||||
export function locateSelectedSegmentName(page: Page, expectedName: string) {
|
export function locateSelectedSegmentName(page: Page, expectedName: string) {
|
||||||
return page.locator('[data-testid="filter-segment-select"]').filter({ hasText: expectedName });
|
return page.locator('[data-testid="filter-segment-select"]').filter({ hasText: expectedName });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a system segment is visible in the dropdown
|
||||||
|
*/
|
||||||
|
export async function expectSystemSegmentVisible(page: Page, segmentName: string) {
|
||||||
|
await page.getByTestId("filter-segment-select").click();
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-testid="filter-segment-select-content"]').getByText("Default")
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.locator('[data-testid="filter-segment-select-content"] [role="menuitem"]')
|
||||||
|
.filter({ hasText: segmentName })
|
||||||
|
).toBeVisible();
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that no segment is currently selected
|
||||||
|
*/
|
||||||
|
export async function expectSegmentCleared(page: Page) {
|
||||||
|
// Check that no segment is selected (button shows default text)
|
||||||
|
const segmentSelect = page.getByTestId("filter-segment-select");
|
||||||
|
const buttonText = await segmentSelect.textContent();
|
||||||
|
expect(buttonText?.trim()).toEqual("Segment");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that a specific segment is currently selected
|
||||||
|
*/
|
||||||
|
export async function expectSegmentSelected(page: Page, segmentName: string) {
|
||||||
|
await expect(locateSelectedSegmentName(page, segmentName)).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicate an existing segment
|
||||||
|
*/
|
||||||
|
export async function duplicateSegment(page: Page, originalName: string, newName: string) {
|
||||||
|
await openSegmentSubmenu(page, originalName);
|
||||||
|
await page.getByTestId("filter-segment-select-submenu-content").getByText("Duplicate").click();
|
||||||
|
await page.getByTestId("duplicate-segment-name").fill(newName);
|
||||||
|
await page.getByRole("button", { name: "Duplicate" }).click();
|
||||||
|
await expect(page.getByText("Filter segment duplicated")).toBeVisible();
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename an existing segment
|
||||||
|
*/
|
||||||
|
export async function renameSegment(page: Page, originalName: string, newName: string) {
|
||||||
|
await openSegmentSubmenu(page, originalName);
|
||||||
|
await page.getByTestId("filter-segment-select-submenu-content").getByText("Rename").click();
|
||||||
|
await page.getByTestId("rename-segment-name").fill(newName);
|
||||||
|
await page.getByRole("button", { name: "Save" }).click();
|
||||||
|
await expect(page.getByText("Filter segment updated")).toBeVisible();
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a segment group (like "Default" or "Personal") is visible
|
||||||
|
*/
|
||||||
|
export async function expectSegmentGroupVisible(page: Page, groupName: string) {
|
||||||
|
await page.getByTestId("filter-segment-select").click();
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-testid="filter-segment-select-content"]').getByText(groupName)
|
||||||
|
).toBeVisible();
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export function createAppsFixture(page: Page) {
|
|||||||
},
|
},
|
||||||
verifyAppsInfoNew: async (app: string, eventTypeId: number) => {
|
verifyAppsInfoNew: async (app: string, eventTypeId: number) => {
|
||||||
await page.goto(`event-types/${eventTypeId}?tabName=apps`);
|
await page.goto(`event-types/${eventTypeId}?tabName=apps`);
|
||||||
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toContainText("Event Setup"); // fix the race condition
|
await expect(page.getByTestId("vertical-tab-basics")).toContainText("Basics"); // fix the race condition
|
||||||
await expect(page.locator(`[data-testid='${app}-app-switch'][data-state="checked"]`)).toBeVisible();
|
await expect(page.locator(`[data-testid='${app}-app-switch'][data-state="checked"]`)).toBeVisible();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ test.describe("private links creation and usage", () => {
|
|||||||
|
|
||||||
// Ensure that private URL is enabled after modifying the event type.
|
// Ensure that private URL is enabled after modifying the event type.
|
||||||
// Additionally, if the slug is changed, ensure that the private URL is updated accordingly.
|
// Additionally, if the slug is changed, ensure that the private URL is updated accordingly.
|
||||||
await page.getByTestId("vertical-tab-event_setup_tab_title").click();
|
await page.getByTestId("vertical-tab-basics").click();
|
||||||
await page.locator("[data-testid=event-title]").first().fill("somethingrandom");
|
await page.locator("[data-testid=event-title]").first().fill("somethingrandom");
|
||||||
await page.locator("[data-testid=event-slug]").first().fill("somethingrandom");
|
await page.locator("[data-testid=event-slug]").first().fill("somethingrandom");
|
||||||
await expect(page.locator('[data-testid="event-slug"]').first()).toHaveValue("somethingrandom");
|
await expect(page.locator('[data-testid="event-slug"]').first()).toHaveValue("somethingrandom");
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { expect } from "@playwright/test";
|
|||||||
import { randomString } from "@calcom/lib/random";
|
import { randomString } from "@calcom/lib/random";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { addFilter, openFilter, clearFilters } from "./filter-helpers";
|
import { clearFilters, applySelectFilter } from "./filter-helpers";
|
||||||
import { test } from "./lib/fixtures";
|
import { test } from "./lib/fixtures";
|
||||||
|
|
||||||
test.describe.configure({ mode: "parallel" });
|
test.describe.configure({ mode: "parallel" });
|
||||||
@@ -202,25 +202,11 @@ test.describe("Insights", async () => {
|
|||||||
await page.locator('[data-testid="org-teams-filter-item"]').nth(1).click();
|
await page.locator('[data-testid="org-teams-filter-item"]').nth(1).click();
|
||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
|
|
||||||
// Choose User filter item from dropdown
|
await applySelectFilter(page, "userId", member.username || "");
|
||||||
await addFilter(page, "userId");
|
|
||||||
|
|
||||||
// Wait for the URL to include userId
|
|
||||||
await page.waitForURL((url) => url.toString().includes("userId"));
|
|
||||||
|
|
||||||
// Click User filter to see a user list
|
|
||||||
await openFilter(page, "userId");
|
|
||||||
|
|
||||||
await page.locator('[data-testid="select-filter-options-userId"]').getByRole("option").nth(0).click();
|
|
||||||
|
|
||||||
await page.locator('[data-testid="select-filter-options-userId"]').getByRole("option").nth(1).click();
|
|
||||||
|
|
||||||
// press escape button to close the filter
|
|
||||||
await page.keyboard.press("Escape");
|
|
||||||
|
|
||||||
await clearFilters(page);
|
await clearFilters(page);
|
||||||
|
|
||||||
await expect(page.url()).not.toContain("userId");
|
await expect(page).not.toHaveURL(/[?&]userId=/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should test download button", async ({ page, users }) => {
|
test("should test download button", async ({ page, users }) => {
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ export const bookTimeSlot = async (
|
|||||||
title?: string;
|
title?: string;
|
||||||
attendeePhoneNumber?: string;
|
attendeePhoneNumber?: string;
|
||||||
expectedStatusCode?: number;
|
expectedStatusCode?: number;
|
||||||
|
isRecurringEvent?: boolean;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
// --- fill form
|
// --- fill form
|
||||||
@@ -166,8 +167,9 @@ export const bookTimeSlot = async (
|
|||||||
if (opts?.attendeePhoneNumber) {
|
if (opts?.attendeePhoneNumber) {
|
||||||
await page.fill('[name="attendeePhoneNumber"]', opts.attendeePhoneNumber ?? "+918888888888");
|
await page.fill('[name="attendeePhoneNumber"]', opts.attendeePhoneNumber ?? "+918888888888");
|
||||||
}
|
}
|
||||||
await submitAndWaitForResponse(page, "/api/book/event", {
|
const url = opts?.isRecurringEvent ? "/api/book/recurring-event" : "/api/book/event";
|
||||||
action: () => page.locator('[name="email"]').press("Enter"),
|
await submitAndWaitForResponse(page, url, {
|
||||||
|
action: () => page.locator('[data-testid="confirm-book-button"]').click(),
|
||||||
expectedStatusCode: opts?.expectedStatusCode,
|
expectedStatusCode: opts?.expectedStatusCode,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -222,10 +224,7 @@ export const createNewSeatedEventType = async (page: Page, args: { eventTitle: s
|
|||||||
const eventTitle = args.eventTitle;
|
const eventTitle = args.eventTitle;
|
||||||
await createNewUserEventType(page, { eventTitle });
|
await createNewUserEventType(page, { eventTitle });
|
||||||
await page.waitForSelector('[data-testid="event-title"]');
|
await page.waitForSelector('[data-testid="event-title"]');
|
||||||
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
|
await expect(page.getByTestId("vertical-tab-basics")).toHaveAttribute("aria-current", "page");
|
||||||
"aria-current",
|
|
||||||
"page"
|
|
||||||
);
|
|
||||||
await page.locator('[data-testid="vertical-tab-event_advanced_tab_title"]').click();
|
await page.locator('[data-testid="vertical-tab-event_advanced_tab_title"]').click();
|
||||||
await page.locator('[data-testid="offer-seats-toggle"]').click();
|
await page.locator('[data-testid="offer-seats-toggle"]').click();
|
||||||
await page.locator('[data-testid="update-eventtype"]').click();
|
await page.locator('[data-testid="update-eventtype"]').click();
|
||||||
|
|||||||
@@ -70,10 +70,7 @@ test.describe("Manage Booking Questions", () => {
|
|||||||
const firstEventTypeElement = $eventTypes.first();
|
const firstEventTypeElement = $eventTypes.first();
|
||||||
|
|
||||||
await firstEventTypeElement.click();
|
await firstEventTypeElement.click();
|
||||||
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
|
await expect(page.getByTestId("vertical-tab-basics")).toHaveAttribute("aria-current", "page");
|
||||||
"aria-current",
|
|
||||||
"page"
|
|
||||||
);
|
|
||||||
await page.getByTestId("vertical-tab-event_advanced_tab_title").click();
|
await page.getByTestId("vertical-tab-event_advanced_tab_title").click();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -117,10 +114,7 @@ test.describe("Manage Booking Questions", () => {
|
|||||||
const firstEventTypeElement = $eventTypes.first();
|
const firstEventTypeElement = $eventTypes.first();
|
||||||
|
|
||||||
await firstEventTypeElement.click();
|
await firstEventTypeElement.click();
|
||||||
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
|
await expect(page.getByTestId("vertical-tab-basics")).toHaveAttribute("aria-current", "page");
|
||||||
"aria-current",
|
|
||||||
"page"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step("Open the 'Name' field dialog", async () => {
|
await test.step("Open the 'Name' field dialog", async () => {
|
||||||
@@ -642,8 +636,8 @@ async function toggleQuestionRequireStatusAndSave({
|
|||||||
await page.locator(`[data-testid="field-${name}"]`).locator('[data-testid="edit-field-action"]').click();
|
await page.locator(`[data-testid="field-${name}"]`).locator('[data-testid="edit-field-action"]').click();
|
||||||
await page
|
await page
|
||||||
.locator('[data-testid="edit-field-dialog"]')
|
.locator('[data-testid="edit-field-dialog"]')
|
||||||
.locator('[data-testid="field-required"] button')
|
.locator('[data-testid="field-required"]')
|
||||||
.locator(`text=${required ? "Yes" : "No"}`)
|
.first()
|
||||||
.click();
|
.click();
|
||||||
await page.locator('[data-testid="field-add-save"]').click();
|
await page.locator('[data-testid="field-add-save"]').click();
|
||||||
await saveEventType(page);
|
await saveEventType(page);
|
||||||
@@ -754,11 +748,8 @@ test.describe("Text area min and max characters text", () => {
|
|||||||
await page.waitForSelector('[data-testid="event-types"]');
|
await page.waitForSelector('[data-testid="event-types"]');
|
||||||
await createNewUserEventType(page, { eventTitle });
|
await createNewUserEventType(page, { eventTitle });
|
||||||
await page.waitForSelector('[data-testid="event-title"]');
|
await page.waitForSelector('[data-testid="event-title"]');
|
||||||
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toContainText("Event Setup"); //fix the race condition
|
await expect(page.getByTestId("vertical-tab-basics")).toContainText("Basics"); //fix the race condition
|
||||||
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
|
await expect(page.getByTestId("vertical-tab-basics")).toHaveAttribute("aria-current", "page");
|
||||||
"aria-current",
|
|
||||||
"page"
|
|
||||||
);
|
|
||||||
await page.getByTestId("vertical-tab-event_advanced_tab_title").click();
|
await page.getByTestId("vertical-tab-event_advanced_tab_title").click();
|
||||||
const insertQuestion = async (questionName: string) => {
|
const insertQuestion = async (questionName: string) => {
|
||||||
const element = page.locator('[data-testid="add-field"]');
|
const element = page.locator('[data-testid="add-field"]');
|
||||||
|
|||||||
@@ -79,11 +79,8 @@ test.describe("Managed Event Types", () => {
|
|||||||
const { adminUser, managedEvent } = await setupManagedEvent({ users });
|
const { adminUser, managedEvent } = await setupManagedEvent({ users });
|
||||||
await adminUser.apiLogin();
|
await adminUser.apiLogin();
|
||||||
await page.goto(`/event-types/${managedEvent.id}?tabName=setup`);
|
await page.goto(`/event-types/${managedEvent.id}?tabName=setup`);
|
||||||
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
|
await expect(page.getByTestId("vertical-tab-basics")).toHaveAttribute("aria-current", "page"); // fix the race condition
|
||||||
"aria-current",
|
await expect(page.getByTestId("vertical-tab-basics")).toContainText("Basics"); //fix the race condition
|
||||||
"page"
|
|
||||||
); // fix the race condition
|
|
||||||
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toContainText("Event Setup"); //fix the race condition
|
|
||||||
await page.locator("#location-select").click();
|
await page.locator("#location-select").click();
|
||||||
const optionText = await getByKey(page, "organizer_default_conferencing_app");
|
const optionText = await getByKey(page, "organizer_default_conferencing_app");
|
||||||
await expect(optionText).toBeVisible();
|
await expect(optionText).toBeVisible();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ test.describe("Onboarding", () => {
|
|||||||
// tests whether the user makes it to /getting-started
|
// tests whether the user makes it to /getting-started
|
||||||
// after login with completedOnboarding false
|
// after login with completedOnboarding false
|
||||||
await page.waitForURL("/getting-started");
|
await page.waitForURL("/getting-started");
|
||||||
await expect(page.locator('text="Next Step"')).toBeVisible(); // Fix race condition
|
await expect(page.locator('text="Connect your calendar"')).toBeVisible(); // Fix race condition
|
||||||
|
|
||||||
await test.step("step 1 - User Settings", async () => {
|
await test.step("step 1 - User Settings", async () => {
|
||||||
// Check required fields
|
// Check required fields
|
||||||
|
|||||||
@@ -780,8 +780,8 @@ const markPhoneNumberAsRequiredAndEmailAsOptional = async (page: Page, eventId:
|
|||||||
|
|
||||||
// Make email as not required
|
// Make email as not required
|
||||||
await page.locator('[data-testid="field-email"] [data-testid="edit-field-action"]').click();
|
await page.locator('[data-testid="field-email"] [data-testid="edit-field-action"]').click();
|
||||||
const emailRequiredFiled = await page.locator('[data-testid="field-required"]');
|
const emailRequiredFiled = await page.locator('[data-testid="field-required"]').first();
|
||||||
await emailRequiredFiled.locator("> :nth-child(2)").click();
|
await emailRequiredFiled.click();
|
||||||
await page.getByTestId("field-add-save").click();
|
await page.getByTestId("field-add-save").click();
|
||||||
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
|
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
|
||||||
action: () => page.locator("[data-testid=update-eventtype]").click(),
|
action: () => page.locator("[data-testid=update-eventtype]").click(),
|
||||||
@@ -790,12 +790,12 @@ const markPhoneNumberAsRequiredAndEmailAsOptional = async (page: Page, eventId:
|
|||||||
|
|
||||||
const markPhoneNumberAsRequiredField = async (page: Page, eventId: number) => {
|
const markPhoneNumberAsRequiredField = async (page: Page, eventId: number) => {
|
||||||
await page.goto(`/event-types/${eventId}?tabName=advanced`);
|
await page.goto(`/event-types/${eventId}?tabName=advanced`);
|
||||||
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toContainText("Event Setup"); // fix the race condition
|
await expect(page.getByTestId("vertical-tab-basics")).toContainText("Basics"); // fix the race condition
|
||||||
|
|
||||||
await page.locator('[data-testid="field-attendeePhoneNumber"] [data-testid="toggle-field"]').click();
|
await page.locator('[data-testid="field-attendeePhoneNumber"] [data-testid="toggle-field"]').click();
|
||||||
await page.locator('[data-testid="field-attendeePhoneNumber"] [data-testid="edit-field-action"]').click();
|
await page.locator('[data-testid="field-attendeePhoneNumber"] [data-testid="edit-field-action"]').click();
|
||||||
const phoneRequiredFiled = await page.locator('[data-testid="field-required"]');
|
const phoneRequiredFiled = await page.locator('[data-testid="field-required"]').first();
|
||||||
await phoneRequiredFiled.locator("> :nth-child(1)").click();
|
await phoneRequiredFiled.click();
|
||||||
await page.getByTestId("field-add-save").click();
|
await page.getByTestId("field-add-save").click();
|
||||||
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
|
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
|
||||||
action: () => page.locator("[data-testid=update-eventtype]").click(),
|
action: () => page.locator("[data-testid=update-eventtype]").click(),
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { expect } from "@playwright/test";
|
||||||
|
import type { Page } from "@playwright/test";
|
||||||
|
|
||||||
|
import { addFilter, selectSegment, expectSegmentCleared, expectSegmentSelected } from "./filter-helpers";
|
||||||
|
import { test } from "./lib/fixtures";
|
||||||
|
|
||||||
|
test.describe.configure({ mode: "parallel" });
|
||||||
|
|
||||||
|
test.afterEach(async ({ users }) => {
|
||||||
|
await users.deleteAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to bookings page with specific status
|
||||||
|
*/
|
||||||
|
async function navigateToBookings(page: Page, status = "upcoming") {
|
||||||
|
// Wait for the bookings API response like existing tests do
|
||||||
|
const bookingsGetResponse = page.waitForResponse((response) =>
|
||||||
|
/\/api\/trpc\/bookings\/get.*/.test(response.url())
|
||||||
|
);
|
||||||
|
await page.goto(`/bookings/${status}`, { waitUntil: "domcontentloaded" });
|
||||||
|
await bookingsGetResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("System Segments", () => {
|
||||||
|
test.describe("Core Functionality", () => {
|
||||||
|
test("My Bookings system segment filters to current user's bookings only", async ({ page, users }) => {
|
||||||
|
const user1 = await users.create({ username: "user1" });
|
||||||
|
|
||||||
|
// Create some bookings for user (this would need booking setup)
|
||||||
|
// For now, we'll test the filter application
|
||||||
|
await user1.apiLogin();
|
||||||
|
await navigateToBookings(page);
|
||||||
|
|
||||||
|
await selectSegment(page, "My Bookings");
|
||||||
|
await expectSegmentSelected(page, "My Bookings");
|
||||||
|
|
||||||
|
// Verify the userId filter is applied (check URL or filter indicators)
|
||||||
|
const url = page.url();
|
||||||
|
expect(url).toContain("activeFilters");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("System segments show only Duplicate option in submenu", async ({ page, users }) => {
|
||||||
|
const user = await users.create();
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
await navigateToBookings(page);
|
||||||
|
|
||||||
|
await page.getByTestId("filter-segment-select").click();
|
||||||
|
await page
|
||||||
|
.locator('[data-testid="filter-segment-select-content"] [role="menuitem"]')
|
||||||
|
.filter({ hasText: "My Bookings" })
|
||||||
|
.locator('[data-testid="filter-segment-select-submenu-button"]')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
const submenu = page.getByTestId("filter-segment-select-submenu-content");
|
||||||
|
await expect(submenu.getByText("Duplicate")).toBeVisible();
|
||||||
|
await expect(submenu.getByText("Rename")).toBeHidden();
|
||||||
|
await expect(submenu.getByText("Delete")).toBeHidden();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("State Persistence", () => {
|
||||||
|
test("System segment selection persists across page visits", async ({ page, users }) => {
|
||||||
|
const owner = await users.create(undefined, { hasTeam: true });
|
||||||
|
await owner.apiLogin();
|
||||||
|
|
||||||
|
// Visit bookings page and select system segment
|
||||||
|
await navigateToBookings(page);
|
||||||
|
await selectSegment(page, "My Bookings");
|
||||||
|
await expectSegmentSelected(page, "My Bookings");
|
||||||
|
|
||||||
|
// Revisit the page - should preserve selection
|
||||||
|
await navigateToBookings(page);
|
||||||
|
await expectSegmentSelected(page, "My Bookings");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Cleared system segment state persists (no default selection after manual clear)", async ({
|
||||||
|
page,
|
||||||
|
users,
|
||||||
|
}) => {
|
||||||
|
const owner = await users.create(undefined, { hasTeam: true });
|
||||||
|
await owner.apiLogin();
|
||||||
|
|
||||||
|
await navigateToBookings(page);
|
||||||
|
|
||||||
|
// Select system segment
|
||||||
|
await selectSegment(page, "My Bookings");
|
||||||
|
await expectSegmentSelected(page, "My Bookings");
|
||||||
|
|
||||||
|
// Unselect system segment
|
||||||
|
await selectSegment(page, "My Bookings");
|
||||||
|
|
||||||
|
// Revisit the page - should NOT have any segment selected
|
||||||
|
await navigateToBookings(page);
|
||||||
|
await expectSegmentCleared(page);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Auto-clear Behavior", () => {
|
||||||
|
test("Manual filter clears system segment selection", async ({ page, users }) => {
|
||||||
|
const user = await users.create(undefined, { hasTeam: true });
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
await navigateToBookings(page);
|
||||||
|
|
||||||
|
// Select system segment
|
||||||
|
await selectSegment(page, "My Bookings");
|
||||||
|
await expectSegmentSelected(page, "My Bookings");
|
||||||
|
|
||||||
|
// Apply manual filter - this should clear system segment
|
||||||
|
await addFilter(page, "eventTypeId");
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
await expectSegmentCleared(page);
|
||||||
|
|
||||||
|
// Verify filter is still applied
|
||||||
|
await expect(page.getByTestId("filter-popover-trigger-eventTypeId")).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
module.exports = {
|
const config = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
config.plugins.autoprefixer = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user