Compare commits
91
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
091327afee | ||
|
|
f1d7d7f02a | ||
|
|
84bd4d3d38 | ||
|
|
0367cf8e41 | ||
|
|
afea724e4d | ||
|
|
a4915184cd | ||
|
|
b7a7797293 | ||
|
|
d0e59f1824 | ||
|
|
67befea66e | ||
|
|
f18ebc4d67 | ||
|
|
5324832f13 | ||
|
|
2d9162a4dd | ||
|
|
d9443162ef | ||
|
|
e8dbab99eb | ||
|
|
cdcc04c8ff | ||
|
|
740a1c991e | ||
|
|
70cfe8d693 | ||
|
|
520e711ca6 | ||
|
|
6d1fe32a39 | ||
|
|
1cc548284d | ||
|
|
d49843de0b | ||
|
|
42efd95476 | ||
|
|
2ec68ef1aa | ||
|
|
6e09489685 | ||
|
|
af07c411ee | ||
|
|
a22ca4758c | ||
|
|
bb3ba88f18 | ||
|
|
ce646c590f | ||
|
|
ba8cca55e8 | ||
|
|
ae738fae35 | ||
|
|
2922c8fe77 | ||
|
|
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 |
No files matched your search
@@ -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
|
||||
secrets: inherit
|
||||
|
||||
e2e-atoms:
|
||||
name: Tests
|
||||
needs: [lint, build, build-atoms, build-api-v2]
|
||||
uses: ./.github/workflows/e2e-atoms.yml
|
||||
secrets: inherit
|
||||
|
||||
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()
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
name: Atoms E2E Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'packages/platform/atoms/**'
|
||||
- 'packages/platform/examples/base/**'
|
||||
- 'apps/api/v2/**'
|
||||
- '.github/workflows/atoms-e2e.yml'
|
||||
|
||||
workflow_call:
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
@@ -25,7 +17,7 @@ env:
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
|
||||
jobs:
|
||||
atoms-e2e:
|
||||
e2e-atoms:
|
||||
timeout-minutes: 15
|
||||
name: Atoms E2E Tests
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
@@ -45,13 +37,13 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: atoms-e2e-test-results
|
||||
name: e2e-atoms-test-results
|
||||
path: test-results
|
||||
retention-days: 7
|
||||
- name: Upload Playwright Report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: atoms-e2e-playwright-report
|
||||
name: e2e-atoms-playwright-report
|
||||
path: playwright-report
|
||||
retention-days: 7
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
with:
|
||||
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
||||
organization-name: calcom
|
||||
ignore-labels: "admin, app-store, ai, authentication, automated-testing, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, dba, devops, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
|
||||
ignore-labels: "admin, app-store, ai, authentication, automated-testing, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, dba, devops, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, self-hosted, teams, webhooks, workflows, zapier"
|
||||
apply-labels-from-issue:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
name: Performance Tests
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
base_url:
|
||||
description: 'Base URL for performance tests'
|
||||
required: false
|
||||
default: 'https://cal.dev'
|
||||
|
||||
jobs:
|
||||
performance-tests:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BASE_URL: ${{ github.event.inputs.base_url || 'https://cal.dev' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup K6
|
||||
uses: grafana/setup-k6-action@v1
|
||||
with:
|
||||
k6-version: '1.0.0'
|
||||
|
||||
- name: Run Smoke Tests
|
||||
uses: grafana/run-k6-action@v1
|
||||
env:
|
||||
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
|
||||
K6_CLOUD_PROJECT_ID: ${{ secrets.K6_CLOUD_PROJECT_ID }}
|
||||
with:
|
||||
path: tests/performance/smoke/*.js
|
||||
cloud-run-locally: true
|
||||
fail-fast: true
|
||||
|
||||
- name: Run Load Tests
|
||||
uses: grafana/run-k6-action@v1
|
||||
env:
|
||||
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
|
||||
K6_CLOUD_PROJECT_ID: ${{ secrets.K6_CLOUD_PROJECT_ID }}
|
||||
with:
|
||||
path: tests/performance/load/*.js
|
||||
cloud-run-locally: true
|
||||
fail-fast: true
|
||||
|
||||
- name: Run Stress Tests
|
||||
uses: grafana/run-k6-action@v1
|
||||
env:
|
||||
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
|
||||
K6_CLOUD_PROJECT_ID: ${{ secrets.K6_CLOUD_PROJECT_ID }}
|
||||
with:
|
||||
path: tests/performance/stress/*.js
|
||||
cloud-run-locally: true
|
||||
fail-fast: true
|
||||
|
||||
- name: Run Spike Tests
|
||||
uses: grafana/run-k6-action@v1
|
||||
env:
|
||||
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
|
||||
K6_CLOUD_PROJECT_ID: ${{ secrets.K6_CLOUD_PROJECT_ID }}
|
||||
with:
|
||||
path: tests/performance/spike/*.js
|
||||
cloud-run-locally: true
|
||||
fail-fast: true
|
||||
@@ -195,6 +195,13 @@ jobs:
|
||||
uses: ./.github/workflows/e2e-embed-react.yml
|
||||
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:
|
||||
name: Analyze Build
|
||||
needs: [build]
|
||||
@@ -204,7 +211,7 @@ jobs:
|
||||
merge-reports:
|
||||
name: Merge reports
|
||||
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
|
||||
secrets: inherit
|
||||
|
||||
@@ -249,6 +256,7 @@ jobs:
|
||||
e2e-embed,
|
||||
e2e-embed-react,
|
||||
e2e-app-store,
|
||||
e2e-atoms,
|
||||
]
|
||||
if: always()
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
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",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.318",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.335",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { TestingModule } from "@nestjs/testing";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
@@ -33,6 +34,7 @@ describe("AppController", () => {
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
let membershipRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
let apiKeyString: string;
|
||||
|
||||
@@ -108,6 +110,14 @@ describe("AppController", () => {
|
||||
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();
|
||||
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 { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { KyselyModule } from "@/modules/kysely/kysely.module";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
@@ -53,6 +54,7 @@ import { Module } from "@nestjs/common";
|
||||
StripeModule,
|
||||
TeamsModule,
|
||||
TeamsEventTypesModule,
|
||||
MembershipsModule,
|
||||
],
|
||||
providers: [
|
||||
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 { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||
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 { 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 { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
@@ -96,6 +100,7 @@ export class BookingsController_2024_08_13 {
|
||||
) {}
|
||||
|
||||
@Post("/")
|
||||
@UseGuards(OptionalApiAuthGuard)
|
||||
@ApiOperation({
|
||||
summary: "Create a booking",
|
||||
description: `
|
||||
@@ -138,9 +143,10 @@ export class BookingsController_2024_08_13 {
|
||||
async createBooking(
|
||||
@Body(new CreateBookingInputPipe())
|
||||
body: CreateBookingInput,
|
||||
@Req() request: Request
|
||||
@Req() request: Request,
|
||||
@GetOptionalUser() user: AuthOptionalUser
|
||||
): 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)) {
|
||||
await this.bookingsService.billBookings(booking);
|
||||
|
||||
+362
-816
File diff suppressed because it is too large.
Load diff
+73
-1
@@ -3,6 +3,7 @@ import { AppModule } from "@/app.module";
|
||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||
import { Locales } from "@/lib/enums/locales";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import {
|
||||
CreateManagedUserData,
|
||||
CreateManagedUserOutput,
|
||||
@@ -80,7 +81,7 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
imports: [AppModule, UsersModule],
|
||||
imports: [AppModule, UsersModule, MembershipsModule],
|
||||
}).compile();
|
||||
|
||||
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 () => {
|
||||
await userRepositoryFixture.delete(firstManagedUser.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 { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.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 { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
@@ -27,7 +26,6 @@ import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@cal
|
||||
import {
|
||||
CancelBookingInput_2024_08_13,
|
||||
CancelSeatedBookingInput_2024_08_13,
|
||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
||||
CreateSeatedBookingOutput_2024_08_13,
|
||||
GetBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
@@ -35,11 +33,9 @@ import {
|
||||
RescheduleSeatedBookingInput_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", () => {
|
||||
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
|
||||
|
||||
describe("Seated bookings", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
@@ -48,7 +44,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
let bookingSeatRepositoryFixture: BookingSeatRepositoryFixture;
|
||||
@@ -58,7 +53,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
let apiKeyString: string;
|
||||
|
||||
let seatedEventTypeId: number;
|
||||
const maxRecurrenceCount = 3;
|
||||
|
||||
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
|
||||
|
||||
@@ -83,7 +77,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
@@ -190,6 +183,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
absent: false,
|
||||
seatUid,
|
||||
bookingFieldsResponses: {
|
||||
guests: [],
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
@@ -264,6 +258,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
absent: false,
|
||||
seatUid: createdSeatedBooking.seatUid,
|
||||
bookingFieldsResponses: {
|
||||
guests: [],
|
||||
name: createdSeatedBooking.attendees[0].name,
|
||||
...createdSeatedBooking.attendees[0].bookingFieldsResponses,
|
||||
},
|
||||
@@ -278,6 +273,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
absent: false,
|
||||
seatUid,
|
||||
bookingFieldsResponses: {
|
||||
guests: [],
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
@@ -517,6 +513,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
absent: false,
|
||||
seatUid,
|
||||
bookingFieldsResponses: {
|
||||
guests: [],
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
|
||||
+76
-1
@@ -21,7 +21,12 @@ import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
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";
|
||||
|
||||
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()}`;
|
||||
let normalEventType: EventType;
|
||||
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 () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
@@ -105,6 +112,17 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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();
|
||||
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 () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.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 { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { getPagination } from "@/lib/pagination/pagination";
|
||||
import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
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 { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
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 { UsersService } from "@/modules/users/services/users.service";
|
||||
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 { Request } from "express";
|
||||
import { DateTime } from "luxon";
|
||||
@@ -95,10 +105,12 @@ export class BookingsService_2024_08_13 {
|
||||
private readonly organizationsRepository: OrganizationsRepository,
|
||||
private readonly teamsRepository: TeamsRepository,
|
||||
private readonly teamsEventTypesRepository: TeamsEventTypesRepository,
|
||||
private readonly membershipsRepository: MembershipsRepository,
|
||||
private readonly membershipsService: MembershipsService,
|
||||
private readonly errorsBookingsService: ErrorsBookingsService_2024_08_13
|
||||
) {}
|
||||
|
||||
async createBooking(request: Request, body: CreateBookingInput) {
|
||||
async createBooking(request: Request, body: CreateBookingInput, authUser: AuthOptionalUser) {
|
||||
let bookingTeamEventType = false;
|
||||
try {
|
||||
const eventType = await this.getBookedEventType(body);
|
||||
@@ -108,6 +120,7 @@ export class BookingsService_2024_08_13 {
|
||||
if (!eventType) {
|
||||
this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body);
|
||||
}
|
||||
await this.checkBookingRequiresAuthenticationSetting(eventType, authUser);
|
||||
|
||||
if (eventType.schedulingType === "MANAGED") {
|
||||
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) {
|
||||
if (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`);
|
||||
}
|
||||
|
||||
this.validateBookingLengthInMinutes(inputBooking, eventType);
|
||||
const lengthInMinutes = inputBooking.lengthInMinutes ?? eventType.length;
|
||||
|
||||
const occurrence = recurringEventSchema.parse(eventType.recurringEvent);
|
||||
const repeatsEvery = occurrence.interval;
|
||||
|
||||
@@ -456,7 +459,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
|
||||
|
||||
for (let i = 0; i < repeatsTimes; i++) {
|
||||
const endTime = startTime.plus({ minutes: eventType.length });
|
||||
const endTime = startTime.plus({ minutes: lengthInMinutes });
|
||||
|
||||
events.push({
|
||||
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 { 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 { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
+5
-1
@@ -498,6 +498,7 @@ describe("Event types Endpoints", () => {
|
||||
lightThemeHex: "#fafafa",
|
||||
},
|
||||
customName: `{Event type title} between {Organiser} and {Scheduler}`,
|
||||
bookingRequiresAuthentication: true,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
@@ -570,6 +571,7 @@ describe("Event types Endpoints", () => {
|
||||
];
|
||||
|
||||
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
|
||||
expect(createdEventType.bookingRequiresAuthentication).toEqual(true);
|
||||
eventType = responseBody.data;
|
||||
});
|
||||
});
|
||||
@@ -1026,6 +1028,7 @@ describe("Event types Endpoints", () => {
|
||||
lightThemeHex: "#fafafa",
|
||||
},
|
||||
customName: `{Event type title} betweennnnnnnnnnn {Organiser} and {Scheduler}`,
|
||||
bookingRequiresAuthentication: false,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
@@ -1120,6 +1123,8 @@ describe("Event types Endpoints", () => {
|
||||
eventType.color = updatedEventType.color;
|
||||
eventType.bookingFields = updatedEventType.bookingFields;
|
||||
eventType.calVideoSettings = updatedEventType.calVideoSettings;
|
||||
|
||||
expect(updatedEventType.bookingRequiresAuthentication).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2028,7 +2033,6 @@ describe("Event types Endpoints", () => {
|
||||
const username = name;
|
||||
let user: User;
|
||||
let legacyEventTypeId1: number;
|
||||
let legacyEventTypeId2: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
|
||||
@@ -132,4 +132,26 @@ export class EventTypesRepository_2024_06_14 {
|
||||
async deleteEventType(eventTypeId: number) {
|
||||
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"
|
||||
| "calVideoSettings"
|
||||
| "hidden"
|
||||
| "bookingRequiresAuthentication"
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
@@ -129,6 +130,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
hideOrganizerEmail,
|
||||
calVideoSettings,
|
||||
hidden,
|
||||
bookingRequiresAuthentication,
|
||||
} = databaseEventType;
|
||||
|
||||
const locations = this.transformLocations(databaseEventType.locations);
|
||||
@@ -206,6 +208,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
hideOrganizerEmail,
|
||||
calVideoSettings,
|
||||
hidden,
|
||||
bookingRequiresAuthentication,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -520,6 +520,8 @@ export const systemBeforeFieldName: NameSystemField = {
|
||||
name: "fullName",
|
||||
type: "text",
|
||||
required: true,
|
||||
label: "",
|
||||
placeholder: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
+2
-2
@@ -292,8 +292,8 @@ describe("transformBookingFieldsInternalToApi", () => {
|
||||
isDefault: true,
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
label: undefined,
|
||||
placeholder: undefined,
|
||||
label: "",
|
||||
placeholder: "",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { PrismaAttributeRepository as PrismaAttributeRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Injectable()
|
||||
export class PrismaAttributeRepository extends PrismaAttributeRepositoryLib {
|
||||
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||
super(dbWrite.prisma as unknown as PrismaClient);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { PrismaHostRepository as PrismaHostRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Injectable()
|
||||
export class PrismaHostRepository extends PrismaHostRepositoryLib {
|
||||
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||
super(dbWrite.prisma as unknown as PrismaClient);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { PrismaAttributeRepository } from "@/lib/repositories/prisma-attribute.repository";
|
||||
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||
import { PrismaHostRepository } from "@/lib/repositories/prisma-host.repository";
|
||||
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
||||
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { LuckyUserService as BaseLuckyUserService } from "@calcom/platform-libraries/bookings";
|
||||
|
||||
@Injectable()
|
||||
export class LuckyUserService extends BaseLuckyUserService {
|
||||
constructor(
|
||||
bookingRepository: PrismaBookingRepository,
|
||||
hostRepository: PrismaHostRepository,
|
||||
oooRepository: PrismaOOORepository,
|
||||
userRepository: PrismaUserRepository,
|
||||
attributeRepository: PrismaAttributeRepository
|
||||
) {
|
||||
super({
|
||||
bookingRepository: bookingRepository,
|
||||
hostRepository: hostRepository,
|
||||
oooRepository: oooRepository,
|
||||
userRepository: userRepository,
|
||||
attributeRepository: attributeRepository,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,12 @@ import { AtomsConferencingAppsController } from "@/modules/atoms/controllers/ato
|
||||
import { AtomsController } from "@/modules/atoms/controllers/atoms.controller";
|
||||
import { AtomsEventTypesController } from "@/modules/atoms/controllers/atoms.event-types.controller";
|
||||
import { AtomsSchedulesController } from "@/modules/atoms/controllers/atoms.schedules.controller";
|
||||
import { AtomsVerificationController } from "@/modules/atoms/controllers/atoms.verification.controller";
|
||||
import { AttributesAtomsService } from "@/modules/atoms/services/attributes-atom.service";
|
||||
import { ConferencingAtomsService } from "@/modules/atoms/services/conferencing-atom.service";
|
||||
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-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 { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||
@@ -32,6 +34,7 @@ import { Module } from "@nestjs/common";
|
||||
AtomsRepository,
|
||||
UsersService,
|
||||
SchedulesAtomsService,
|
||||
VerificationAtomsService,
|
||||
RedisService,
|
||||
],
|
||||
exports: [EventTypesAtomService],
|
||||
@@ -40,6 +43,7 @@ import { Module } from "@nestjs/common";
|
||||
AtomsEventTypesController,
|
||||
AtomsConferencingAppsController,
|
||||
AtomsSchedulesController,
|
||||
AtomsVerificationController,
|
||||
],
|
||||
})
|
||||
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 { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
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 { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
@@ -22,7 +21,6 @@ import { PassportModule } from "@nestjs/passport";
|
||||
MembershipsModule,
|
||||
TokensModule,
|
||||
DeploymentsModule,
|
||||
ProfilesModule,
|
||||
],
|
||||
providers: [NextAuthGuard, NextAuthStrategy, ApiAuthGuard, ApiAuthStrategy, OAuthFlowService],
|
||||
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 { createParamDecorator } from "@nestjs/common";
|
||||
|
||||
export type AuthOptionalUser = ApiAuthGuardUser | null;
|
||||
|
||||
export const GetOptionalUser = createParamDecorator<
|
||||
keyof ApiAuthGuardUser | (keyof ApiAuthGuardUser)[],
|
||||
ExecutionContext
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { DeploymentsRepository } from "@/modules/deployments/deployments.repository";
|
||||
import { DeploymentsService } from "@/modules/deployments/deployments.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 { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.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 { createRequest } from "node-mocks-http";
|
||||
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 { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
@@ -45,6 +47,7 @@ describe("ApiAuthStrategy", () => {
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
let oAuthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
let membershipRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
const validApiKeyEmail = `api-auth-api-key-user-${randomString()}@api.com`;
|
||||
const validAccessTokenEmail = `api-auth-access-token-user-${randomString()}@api.com`;
|
||||
@@ -65,6 +68,7 @@ describe("ApiAuthStrategy", () => {
|
||||
}),
|
||||
ProfilesModule,
|
||||
TokensModule,
|
||||
MembershipsModule,
|
||||
],
|
||||
providers: [
|
||||
MockedRedisService,
|
||||
@@ -91,6 +95,7 @@ describe("ApiAuthStrategy", () => {
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(module);
|
||||
oAuthClientRepositoryFixture = new OAuthClientRepositoryFixture(module);
|
||||
profilesRepositoryFixture = new ProfileRepositoryFixture(module);
|
||||
membershipRepositoryFixture = new MembershipRepositoryFixture(module);
|
||||
organization = await teamRepositoryFixture.create({ name: `api-auth-organization-1-${randomString()}` });
|
||||
organizationTwo = await teamRepositoryFixture.create({
|
||||
name: `api-auth-organization-2-${randomString()}`,
|
||||
@@ -121,6 +126,20 @@ describe("ApiAuthStrategy", () => {
|
||||
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 = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
|
||||
@@ -4,9 +4,9 @@ import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed";
|
||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
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 { TokensService } from "@/modules/tokens/tokens.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 apiKeyRepository: ApiKeysRepository,
|
||||
private readonly oauthRepository: OAuthClientRepository,
|
||||
private readonly profilesRepository: ProfilesRepository,
|
||||
private readonly usersService: UsersService
|
||||
private readonly usersService: UsersService,
|
||||
private readonly membershipsRepository: MembershipsRepository
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -172,7 +172,9 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
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) {
|
||||
throw new UnauthorizedException(
|
||||
|
||||
@@ -89,7 +89,6 @@ export class BillingService implements OnModuleDestroy {
|
||||
teamId: teamId.toString(),
|
||||
plan: plan.toString(),
|
||||
},
|
||||
currency: "usd",
|
||||
subscription_data: {
|
||||
metadata: {
|
||||
teamId: teamId.toString(),
|
||||
@@ -117,7 +116,6 @@ export class BillingService implements OnModuleDestroy {
|
||||
teamId: teamId.toString(),
|
||||
plan: plan.toString(),
|
||||
},
|
||||
currency: "usd",
|
||||
});
|
||||
|
||||
if (!url) throw new InternalServerErrorException("Failed to create Stripe session.");
|
||||
@@ -403,7 +401,6 @@ export class BillingService implements OnModuleDestroy {
|
||||
fromReschedule?: string | null;
|
||||
}
|
||||
) {
|
||||
|
||||
if (this.configService.get("e2e")) {
|
||||
return true;
|
||||
}
|
||||
@@ -465,7 +462,6 @@ export class BillingService implements OnModuleDestroy {
|
||||
|
||||
async onModuleDestroy() {
|
||||
try {
|
||||
|
||||
await this.billingQueue.close();
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { MembershipRole } from "@calcom/prisma/client";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
@Injectable()
|
||||
export class MembershipsRepository {
|
||||
@@ -20,6 +20,36 @@ export class MembershipsRepository {
|
||||
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) {
|
||||
const membership = await this.dbRead.prisma.membership.findUnique({
|
||||
where: {
|
||||
@@ -60,6 +90,26 @@ export class MembershipsRepository {
|
||||
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) {
|
||||
const membership = await this.dbRead.prisma.membership.create({
|
||||
data: {
|
||||
@@ -73,4 +123,46 @@ export class MembershipsRepository {
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
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 { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||
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 { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
@@ -63,6 +64,7 @@ describe("OAuthFlow Endpoints", () => {
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let oAuthClientsRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
let membershipRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
let user: User;
|
||||
let organization: Team;
|
||||
@@ -89,6 +91,7 @@ describe("OAuthFlow Endpoints", () => {
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
usersRepositoryFixtures = new UserRepositoryFixture(moduleRef);
|
||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
|
||||
user = await usersRepositoryFixtures.create({
|
||||
email: userEmail,
|
||||
@@ -104,6 +107,13 @@ describe("OAuthFlow Endpoints", () => {
|
||||
movedFromUser: { connect: { id: user.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);
|
||||
});
|
||||
|
||||
|
||||
+7
@@ -39,6 +39,7 @@ import {
|
||||
HttpStatus,
|
||||
NotFoundException,
|
||||
Query,
|
||||
Logger,
|
||||
} from "@nestjs/common";
|
||||
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_API_KEY_HEADER)
|
||||
export class OrganizationsEventTypesController {
|
||||
private readonly logger = new Logger("OrganizationsEventTypesController");
|
||||
|
||||
constructor(
|
||||
private readonly organizationsEventTypesService: OrganizationsEventTypesService,
|
||||
private readonly inputService: InputOrganizationsEventTypesService,
|
||||
@@ -88,6 +91,10 @@ export class OrganizationsEventTypesController {
|
||||
teamId,
|
||||
bodyEventType
|
||||
);
|
||||
this.logger.debug(
|
||||
"nl debug - create org team event type - transformedBody",
|
||||
JSON.stringify(transformedBody, null, 2)
|
||||
);
|
||||
|
||||
const eventType = await this.organizationsEventTypesService.createOrganizationTeamEventType(
|
||||
user,
|
||||
|
||||
+4
@@ -725,6 +725,8 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
isDefault: true,
|
||||
type: "name",
|
||||
slug: "name",
|
||||
label: "",
|
||||
placeholder: "",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
@@ -821,6 +823,8 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
isDefault: true,
|
||||
type: "name",
|
||||
slug: "name",
|
||||
label: "",
|
||||
placeholder: "",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
|
||||
+4
@@ -47,6 +47,10 @@ export class OrganizationsEventTypesService {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ type Input = Pick<
|
||||
| "team"
|
||||
| "calVideoSettings"
|
||||
| "hidden"
|
||||
| "bookingRequiresAuthentication"
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
|
||||
+5
@@ -43,6 +43,8 @@ export class OrganizationsTeamsSchedulesController {
|
||||
@DocsTags("Orgs / Teams / Schedules")
|
||||
@ApiOperation({ summary: "Get all team member schedules" })
|
||||
async getTeamSchedules(
|
||||
// note(Lauirs): putting orgId so swagger is generacted correctly
|
||||
@Param("orgId", ParseIntPipe) _orgId: number,
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@Query() queryParams: SkipTakePagination
|
||||
): Promise<GetSchedulesOutput_2024_06_11> {
|
||||
@@ -65,6 +67,9 @@ export class OrganizationsTeamsSchedulesController {
|
||||
summary: "Get schedules of a team member",
|
||||
})
|
||||
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
|
||||
): Promise<GetSchedulesOutput_2024_06_11> {
|
||||
const schedules = await this.schedulesService.getUserSchedules(userId);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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 { 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 { 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";
|
||||
@@ -26,7 +29,6 @@ import {
|
||||
ApiResponse as DocsResponse,
|
||||
ApiQuery,
|
||||
} from "@nestjs/swagger";
|
||||
import { User } from "@prisma/client";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
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)
|
||||
async reserveSlot(
|
||||
@Body() body: ReserveSlotInput_2024_09_04,
|
||||
@GetOptionalUser() user: User
|
||||
@GetOptionalUser() user: AuthOptionalUser
|
||||
): Promise<ReserveSlotOutputResponse_2024_09_04> {
|
||||
const reservedSlot = await this.slotsService.reserveSlot(body, user?.id);
|
||||
|
||||
|
||||
@@ -133,6 +133,7 @@ export class TeamsEventTypesService {
|
||||
});
|
||||
|
||||
const eventType = await this.teamsEventTypesRepository.getEventTypeById(eventTypeId);
|
||||
this.logger.debug("nl debug - update team event type - eventType", JSON.stringify(eventType, null, 2));
|
||||
|
||||
if (!eventType) {
|
||||
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": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsStripeController_getTeamStripeConnectUrl",
|
||||
@@ -5720,6 +5811,22 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "orgId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "teamId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"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": {
|
||||
"post": {
|
||||
"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": {
|
||||
"type": "array",
|
||||
"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": {
|
||||
"type": "number",
|
||||
"example": 10
|
||||
@@ -18195,6 +18382,8 @@
|
||||
"successRedirectUrl",
|
||||
"isInstantEvent",
|
||||
"scheduleId",
|
||||
"hidden",
|
||||
"bookingRequiresAuthentication",
|
||||
"ownerId",
|
||||
"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": {
|
||||
"type": "array",
|
||||
"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": {
|
||||
"type": "string",
|
||||
"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": {
|
||||
"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": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -22589,6 +22809,11 @@
|
||||
},
|
||||
"unit": {
|
||||
"type": "object",
|
||||
"enum": [
|
||||
"hour",
|
||||
"minute",
|
||||
"day"
|
||||
],
|
||||
"description": "Unit for the offset time",
|
||||
"example": "hour"
|
||||
}
|
||||
@@ -22612,6 +22837,9 @@
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "beforeEvent",
|
||||
"enum": [
|
||||
"beforeEvent"
|
||||
],
|
||||
"description": "Trigger type for the workflow",
|
||||
"example": "beforeEvent"
|
||||
}
|
||||
@@ -22635,6 +22863,9 @@
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "afterEvent",
|
||||
"enum": [
|
||||
"afterEvent"
|
||||
],
|
||||
"description": "Trigger type for the workflow",
|
||||
"example": "afterEvent"
|
||||
}
|
||||
@@ -22650,6 +22881,9 @@
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "eventCancelled",
|
||||
"enum": [
|
||||
"eventCancelled"
|
||||
],
|
||||
"description": "Trigger type for the workflow"
|
||||
}
|
||||
},
|
||||
@@ -22663,6 +22897,9 @@
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "newEvent",
|
||||
"enum": [
|
||||
"newEvent"
|
||||
],
|
||||
"description": "Trigger type for the workflow"
|
||||
}
|
||||
},
|
||||
@@ -22676,6 +22913,9 @@
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "rescheduleEvent",
|
||||
"enum": [
|
||||
"rescheduleEvent"
|
||||
],
|
||||
"description": "Trigger type for the workflow"
|
||||
}
|
||||
},
|
||||
@@ -22697,6 +22937,9 @@
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "afterGuestsCalVideoNoShow",
|
||||
"enum": [
|
||||
"afterGuestsCalVideoNoShow"
|
||||
],
|
||||
"description": "Trigger type for the workflow",
|
||||
"example": "afterGuestsCalVideoNoShow"
|
||||
}
|
||||
@@ -22720,6 +22963,9 @@
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "afterHostsCalVideoNoShow",
|
||||
"enum": [
|
||||
"afterHostsCalVideoNoShow"
|
||||
],
|
||||
"description": "Trigger type for the workflow",
|
||||
"example": "afterHostsCalVideoNoShow"
|
||||
}
|
||||
@@ -23314,6 +23560,17 @@
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "object",
|
||||
"enum": [
|
||||
[
|
||||
"beforeEvent",
|
||||
"eventCancelled",
|
||||
"newEvent",
|
||||
"afterEvent",
|
||||
"rescheduleEvent",
|
||||
"afterHostsCalVideoNoShow",
|
||||
"afterGuestsCalVideoNoShow"
|
||||
]
|
||||
],
|
||||
"description": "Trigger type for the workflow"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -15,7 +15,6 @@ export class ApiKeysRepositoryFixture {
|
||||
async createApiKey(userId: number, expiresAt: Date | null, teamId?: number) {
|
||||
const keyString = randomBytes(16).toString("hex");
|
||||
const hashedKey = createHash("sha256").update(keyString).digest("hex");
|
||||
console.log("createApiKey Fixutre", { hashedKey, keyString });
|
||||
const apiKey = await this.prismaWriteClient.apiKey.create({
|
||||
data: {
|
||||
userId,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CustomI18nProvider } from "app/CustomI18nProvider";
|
||||
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 { cookies, headers } from "next/headers";
|
||||
|
||||
@@ -15,20 +15,34 @@ import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
|
||||
import LegacyPage 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 isGloballyEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally(
|
||||
const isTeamFeatureEnabled = await featuresRepository.checkIfTeamHasFeature(
|
||||
teamId,
|
||||
"team-booking-page-cache"
|
||||
);
|
||||
return isGloballyEnabled && searchParams.experimentalTeamBookingPageCache === "true";
|
||||
return isTeamFeatureEnabled;
|
||||
}
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
if (await isCachedTeamBookingEnabled(await searchParams)) {
|
||||
if (await isCachedTeamBookingEnabled(await params, await searchParams)) {
|
||||
return await generateCachedMetadata({ params, searchParams });
|
||||
}
|
||||
|
||||
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
const { booking, isSEOIndexable, eventData, isBrandingHidden } = props;
|
||||
@@ -68,7 +82,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
if (await isCachedTeamBookingEnabled(await searchParams)) {
|
||||
if (await isCachedTeamBookingEnabled(await params, await 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({
|
||||
slug: params?.slug,
|
||||
type: params?.type,
|
||||
@@ -74,7 +74,7 @@ const _getMultipleDurationValue = (
|
||||
};
|
||||
|
||||
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);
|
||||
if (!teamData) return {}; // should never happen
|
||||
@@ -123,7 +123,7 @@ export const generateMetadata = 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);
|
||||
|
||||
// Handle org redirects
|
||||
|
||||
@@ -13,6 +13,7 @@ import { getTeamEventType } from "@calcom/features/eventtypes/lib/getTeamEventTy
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { NEXTJS_CACHE_TTL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { SchedulingType } from "@calcom/prisma/enums";
|
||||
@@ -158,3 +159,14 @@ export async function getCRMData(
|
||||
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 type { PageProps } from "app/_types";
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { validStatuses } from "~/bookings/lib/validStatuses";
|
||||
import BookingsList from "~/bookings/views/bookings-listing-view";
|
||||
|
||||
@@ -26,10 +31,11 @@ const Page = async ({ params }: PageProps) => {
|
||||
redirect("/bookings/upcoming");
|
||||
}
|
||||
const t = await getTranslate();
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
|
||||
return (
|
||||
<ShellMainAppDir heading={t("bookings")} subtitle={t("bookings_description")}>
|
||||
<BookingsList status={parsed.data.status} />
|
||||
<BookingsList status={parsed.data.status} userId={session?.user?.id} />
|
||||
</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;
|
||||
@@ -1,7 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
const Page = () => {
|
||||
redirect("/apps/installed/calendar");
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -1,7 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
const ServerPage = async () => {
|
||||
redirect("/apps/routing-forms/forms");
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
@@ -1,17 +0,0 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("apps"),
|
||||
(t) => t("admin_apps_description"),
|
||||
undefined,
|
||||
undefined,
|
||||
"/settings/admin/apps"
|
||||
);
|
||||
|
||||
const Page = () => {
|
||||
redirect("/settings/admin/apps/calendar");
|
||||
};
|
||||
|
||||
export default Page;
|
||||
+7
-5
@@ -21,11 +21,13 @@ const Page = async () => {
|
||||
createRouterCaller(appsRouter),
|
||||
]);
|
||||
|
||||
const connectedCalendars = await calendarsCaller.connectedCalendars();
|
||||
const installedCalendars = await appsCaller.integrations({
|
||||
variant: "calendar",
|
||||
onlyInstalled: true,
|
||||
});
|
||||
const [connectedCalendars, installedCalendars] = await Promise.all([
|
||||
calendarsCaller.connectedCalendars(),
|
||||
appsCaller.integrations({
|
||||
variant: "calendar",
|
||||
onlyInstalled: true,
|
||||
}),
|
||||
]);
|
||||
return (
|
||||
<CalendarListContainer connectedCalendars={connectedCalendars} installedCalendars={installedCalendars} />
|
||||
);
|
||||
|
||||
+2
-2
@@ -21,9 +21,9 @@ export const generateMetadata = async () =>
|
||||
);
|
||||
|
||||
const Page = async () => {
|
||||
const t = await getTranslate();
|
||||
const [t, _headers, _cookies] = await Promise.all([getTranslate(), headers(), cookies()]);
|
||||
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
const session = await getServerSession({ req: buildLegacyRequest(_headers, _cookies) });
|
||||
|
||||
if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
|
||||
return redirect("/settings/profile");
|
||||
|
||||
+1
-1
@@ -6,9 +6,9 @@ import type { Resource } from "@calcom/features/pbac/domain/types/permission-reg
|
||||
import { PERMISSION_REGISTRY, CrudAction } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Checkbox, Label } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
import { Checkbox, Label } from "@calcom/ui/form";
|
||||
|
||||
import { usePermissions } from "./usePermissions";
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui/button";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
|
||||
import { useRoleSheetState } from "../hooks/useRoleQueryStates";
|
||||
|
||||
|
||||
+2
-2
@@ -14,10 +14,10 @@ import {
|
||||
} from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/button";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Form, TextField, Checkbox, Label } from "@calcom/ui/components/form";
|
||||
import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from "@calcom/ui/components/sheet";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { Form, TextField, Checkbox, Label } from "@calcom/ui/form";
|
||||
|
||||
import { revalidateTeamRoles } from "../actions";
|
||||
import { AdvancedPermissionGroup } from "./AdvancedPermissionGroup";
|
||||
|
||||
+2
-2
@@ -2,9 +2,9 @@
|
||||
|
||||
import type { Scope } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Badge } from "@calcom/ui/badge";
|
||||
import { Button } from "@calcom/ui/button";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownMenuContent,
|
||||
|
||||
-12
@@ -44,17 +44,5 @@ describe("usePermissions", () => {
|
||||
|
||||
expect(getResourcePermissionLevel("eventType", permissions)).toBe("all");
|
||||
});
|
||||
|
||||
it("should return 'all' for resource with manage permission", () => {
|
||||
const permissions = ["eventType.manage"];
|
||||
|
||||
expect(getResourcePermissionLevel("eventType", permissions)).toBe("all");
|
||||
});
|
||||
|
||||
it("should return 'all' for resource with manage permission even if other permissions are missing", () => {
|
||||
const permissions = ["eventType.manage", "eventType.read"]; // Has manage and read, but missing create, update, delete
|
||||
|
||||
expect(getResourcePermissionLevel("eventType", permissions)).toBe("all");
|
||||
});
|
||||
});
|
||||
});
|
||||
+5
-11
@@ -51,20 +51,14 @@ export function usePermissions(): UsePermissionsReturn {
|
||||
return "all";
|
||||
}
|
||||
|
||||
// Check if user has manage permission for this resource
|
||||
const hasManagePermission = permissions.includes(`${resource}.manage`);
|
||||
if (hasManagePermission) {
|
||||
return "all";
|
||||
}
|
||||
|
||||
// Filter out internal keys like _resource and manage when checking for individual permissions
|
||||
const crudPermissions = Object.keys(resourceConfig)
|
||||
.filter((action) => !action.startsWith("_") && action !== "manage")
|
||||
// Filter out internal keys like _resource when checking permissions
|
||||
const allResourcePerms = Object.keys(resourceConfig)
|
||||
.filter((action) => !action.startsWith("_"))
|
||||
.map((action) => `${resource}.${action}`);
|
||||
const hasAllCrudPerms = crudPermissions.every((p) => permissions.includes(p));
|
||||
const hasAllPerms = allResourcePerms.every((p) => permissions.includes(p));
|
||||
const hasReadPerm = permissions.includes(`${resource}.${CrudAction.Read}`);
|
||||
|
||||
if (hasAllCrudPerms) return "all";
|
||||
if (hasAllPerms) return "all";
|
||||
if (hasReadPerm) return "read";
|
||||
return "none";
|
||||
};
|
||||
|
||||
+1
-3
@@ -40,9 +40,7 @@ const getCachedRoles = unstable_cache(
|
||||
const Page = async () => {
|
||||
const orgCaller = await createRouterCaller(viewerOrganizationsRouter);
|
||||
const [org, teams] = await Promise.all([orgCaller.listCurrent(), orgCaller.getTeams()]);
|
||||
const attributes = await getCachedAttributes(org.id);
|
||||
|
||||
const roles = await getCachedRoles(org.id);
|
||||
const [attributes, roles] = await Promise.all([getCachedAttributes(org.id), getCachedRoles(org.id)]);
|
||||
|
||||
const facetedTeamValues = {
|
||||
roles,
|
||||
|
||||
@@ -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 (
|
||||
meeting: MeetingImageProps,
|
||||
getTitle: (t: TFunction<string, undefined>) => string,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import { parseRequestData } from "app/api/parseRequestData";
|
||||
import * as jose from "jose";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import * as dummy from "openid-client";
|
||||
import { uuid } from "short-uuid";
|
||||
|
||||
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";
|
||||
|
||||
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 log = logger.getSubLogger({ prefix: ["[SAML token]"] });
|
||||
|
||||
|
||||
@@ -30,6 +30,10 @@ const querySchema = z.object({
|
||||
const decryptedSchema = z.object({
|
||||
bookingUid: z.string(),
|
||||
userId: z.number().int(),
|
||||
platformClientId: z.string().optional(),
|
||||
platformRescheduleUrl: z.string().optional(),
|
||||
platformCancelUrl: z.string().optional(),
|
||||
platformBookingUrl: z.string().optional(),
|
||||
});
|
||||
|
||||
// Move the sessionGetter function outside the GET function
|
||||
@@ -64,7 +68,14 @@ async function handler(request: NextRequest) {
|
||||
symmetricDecrypt(decodeURIComponent(token), process.env.CALENDSO_ENCRYPTION_KEY || "")
|
||||
);
|
||||
|
||||
const { bookingUid, userId } = decryptedSchema.parse(decryptedData);
|
||||
const {
|
||||
bookingUid,
|
||||
userId,
|
||||
platformClientId,
|
||||
platformRescheduleUrl,
|
||||
platformCancelUrl,
|
||||
platformBookingUrl,
|
||||
} = decryptedSchema.parse(decryptedData);
|
||||
|
||||
const booking = await prisma.booking.findUniqueOrThrow({
|
||||
where: { uid: bookingUid },
|
||||
@@ -97,6 +108,14 @@ async function handler(request: NextRequest) {
|
||||
recurringEventId: booking.recurringEventId || undefined,
|
||||
confirmed: action === DirectAction.ACCEPT,
|
||||
reason,
|
||||
platformClientParams: platformClientId
|
||||
? {
|
||||
platformClientId,
|
||||
platformRescheduleUrl,
|
||||
platformCancelUrl,
|
||||
platformBookingUrl,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
} catch (e) {
|
||||
let message = "Error confirming booking";
|
||||
|
||||
@@ -193,25 +193,27 @@ async function getHandler(request: NextRequest) {
|
||||
const response = await fetch(filteredLogo);
|
||||
const arrayBuffer = await response.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) {
|
||||
const { detectContentType, optimizeImage } = await import("next/dist/server/image-optimizer");
|
||||
|
||||
buffer = await optimizeImage({
|
||||
const { resizeImage } = await import("@calcom/lib/server/imageUtils");
|
||||
const { buffer: outBuffer, contentType: outContentType } = await resizeImage({
|
||||
buffer,
|
||||
contentType: (await detectContentType(buffer)) ?? "image/jpeg",
|
||||
quality: 100,
|
||||
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
|
||||
const imageResponse = new NextResponse(buffer as BodyInit);
|
||||
|
||||
// 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");
|
||||
|
||||
return imageResponse;
|
||||
|
||||
@@ -12,7 +12,7 @@ import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
@@ -50,7 +50,7 @@ async function getHandler(req: NextRequest, { params }: { params: Promise<Params
|
||||
if (!team) {
|
||||
const prevTeam = await prisma.team.findFirstOrThrow({ where: { id } });
|
||||
|
||||
metadata = teamMetadataSchema.safeParse(prevTeam.metadata);
|
||||
metadata = teamMetadataStrictSchema.safeParse(prevTeam.metadata);
|
||||
if (!metadata.success) {
|
||||
throw new HttpError({ statusCode: 400, message: "Invalid team metadata" });
|
||||
}
|
||||
@@ -80,7 +80,7 @@ async function getHandler(req: NextRequest, { params }: { params: Promise<Params
|
||||
}
|
||||
|
||||
if (!metadata) {
|
||||
metadata = teamMetadataSchema.safeParse(team.metadata);
|
||||
metadata = teamMetadataStrictSchema.safeParse(team.metadata);
|
||||
if (!metadata.success) {
|
||||
throw new HttpError({ statusCode: 400, message: "Invalid team metadata" });
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
redirect(process.env.NEXT_PUBLIC_WEBAPP_URL || "https://app.cal.com");
|
||||
}
|
||||
+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 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 { 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 calFont = localFont({
|
||||
src: "../../fonts/CalSans-SemiBold.woff2",
|
||||
@@ -16,25 +26,16 @@ const calFont = localFont({
|
||||
display: "swap",
|
||||
weight: "600",
|
||||
});
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => t("icon_showcase"),
|
||||
() => "",
|
||||
undefined,
|
||||
undefined,
|
||||
"/icons"
|
||||
);
|
||||
};
|
||||
export default async function IconsPage() {
|
||||
|
||||
export default function IconsPage() {
|
||||
const icons = Array.from(lucideIconList).sort() as IconName[];
|
||||
const t = await getTranslate();
|
||||
|
||||
return (
|
||||
<div className={`${interFont.variable} ${calFont.variable}`}>
|
||||
<div className="bg-subtle flex h-screen">
|
||||
<IconSprites />
|
||||
<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="Filled Icons"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { TrpcProvider } from "app/_trpc/trpc-provider";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import CacheProvider from "react-inlinesvg/provider";
|
||||
|
||||
import { WebPushProvider } from "@calcom/features/notifications/WebPushContext";
|
||||
@@ -17,11 +18,13 @@ type ProvidersProps = {
|
||||
};
|
||||
export function Providers({ isEmbed, children, nonce }: ProvidersProps) {
|
||||
const isBookingPage = useIsBookingPage();
|
||||
const pathname = usePathname();
|
||||
const isOnboardingPage = pathname?.startsWith("/getting-started");
|
||||
|
||||
return (
|
||||
<SessionProvider>
|
||||
<TrpcProvider>
|
||||
{!isBookingPage ? <PlainChat nonce={nonce} /> : null}
|
||||
{!isBookingPage && !isOnboardingPage ? <PlainChat nonce={nonce} /> : null}
|
||||
{!isEmbed && !isBookingPage && <NotificationSoundHandler />}
|
||||
{/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */}
|
||||
<CacheProvider>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
const Page = () => {
|
||||
redirect("/apps/routing-forms/forms");
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -218,7 +218,7 @@ export const ReassignDialog = ({
|
||||
placeholder={t("search")}
|
||||
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) => (
|
||||
<label
|
||||
key={member.value}
|
||||
|
||||
@@ -539,18 +539,11 @@ const NewRoutingManager = ({
|
||||
url: chosenEventUrls.eventBookingAbsoluteUrl,
|
||||
});
|
||||
|
||||
console.log("SETTING NEW VALUE", {
|
||||
type: "reschedule_to_different_event_new_tab",
|
||||
reschedulerWindow,
|
||||
newBooking: null,
|
||||
});
|
||||
reroutingState.setValue({
|
||||
type: "reschedule_to_different_event_new_tab",
|
||||
reschedulerWindow,
|
||||
newBooking: null,
|
||||
});
|
||||
|
||||
console.log("VALUE NOW", JSON.stringify(reroutingState));
|
||||
}
|
||||
|
||||
function reroutingPreview() {
|
||||
@@ -910,7 +903,7 @@ export const RerouteDialog = ({ isOpenDialog, setIsOpenDialog, booking }: Rerout
|
||||
|
||||
return (
|
||||
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
||||
<DialogContent enableOverflow preventCloseOnOutsideClick>
|
||||
<DialogContent preventCloseOnOutsideClick>
|
||||
<DialogHeader title={t("reroute_booking")} subtitle={t("reroute_booking_description")} />
|
||||
<RerouteDialogContentAndFooter
|
||||
booking={teamEventTypeBooking}
|
||||
|
||||
@@ -84,6 +84,7 @@ const AppConnectionItem = (props: IAppConnectionItem) => {
|
||||
{t("this_app_requires_connected_account", {
|
||||
appName: title,
|
||||
dependencyName: dependency.name,
|
||||
interpolation: { escapeValue: false },
|
||||
})}
|
||||
</span>
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
||||
loading={isPageLoading}
|
||||
onClick={() => nextStep()}
|
||||
disabled={disabledNextButton}>
|
||||
{firstCalendar ? `${t("continue")}` : `${t("next_step_text")}`}
|
||||
{firstCalendar ? `${t("connect_your_video")}` : `${t("connect_calendar_first")}`}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -105,7 +105,7 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
|
||||
disabled={!hasAnyInstalledVideoApps}
|
||||
loading={isPageLoading}
|
||||
onClick={() => nextStep()}>
|
||||
{t("next_step_text")}
|
||||
{t("set_availability")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -81,7 +81,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
|
||||
className="mt-2 w-full justify-center p-2 text-sm sm:mt-8"
|
||||
loading={availabilityForm.formState.isSubmitting}
|
||||
disabled={availabilityForm.formState.isSubmitting}>
|
||||
{t("next_step_text")}
|
||||
{t("complete_profile")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
@@ -164,7 +164,7 @@ const UserProfile = ({ user }: UserProfileProps) => {
|
||||
EndIcon="arrow-right"
|
||||
type="submit"
|
||||
className="mt-8 w-full items-center justify-center">
|
||||
{t("finish")}
|
||||
{t("finish_and_start")}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -88,6 +88,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="John Doe"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
/>
|
||||
@@ -121,7 +122,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
||||
className="mt-8 flex w-full flex-row justify-center"
|
||||
loading={mutation.isPending}
|
||||
disabled={mutation.isPending}>
|
||||
{t("next_step_text")}
|
||||
{t("connect_your_calendar")}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,6 @@ export function NotificationSoundHandler() {
|
||||
const response = await fetch("/ring.mp3");
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
audioBufferRef.current = await audioContextRef.current.decodeAudioData(arrayBuffer);
|
||||
console.log("Audio file loaded and decoded");
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -62,7 +61,6 @@ export function NotificationSoundHandler() {
|
||||
|
||||
source.loop = true;
|
||||
source.start(0);
|
||||
console.log("Sound started playing");
|
||||
|
||||
setTimeout(() => {
|
||||
if (sourceRef.current === source) {
|
||||
@@ -101,7 +99,7 @@ export function NotificationSoundHandler() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!("serviceWorker" in navigator)) {
|
||||
console.log("ServiceWorker not available");
|
||||
console.warn("ServiceWorker not available");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -120,6 +120,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
|
||||
<TextField
|
||||
ref={usernameRef}
|
||||
name="username"
|
||||
placeholder="john"
|
||||
value={inputUsernameValue}
|
||||
autoComplete="none"
|
||||
autoCapitalize="none"
|
||||
|
||||
@@ -3,42 +3,48 @@
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN_CLIENT,
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN_CLIENT,
|
||||
|
||||
sampleRate: parseFloat(process.env.SENTRY_SAMPLE_RATE ?? "1.0") || 1.0,
|
||||
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
|
||||
tracesSampleRate: parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE ?? "0.0") || 0.0,
|
||||
sampleRate: parseFloat(process.env.SENTRY_SAMPLE_RATE ?? "1.0") || 1.0,
|
||||
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
|
||||
tracesSampleRate: parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE ?? "0.0") || 0.0,
|
||||
|
||||
// Define how likely Replay events are sampled.
|
||||
// This sets the sample rate to be 10%. You may want this to be 100% while
|
||||
// in development and sample at a lower rate in production
|
||||
// replaysSessionSampleRate: parseFloat(process.env.SENTRY_REPLAYS_SESSION_SAMPLE_RATE ?? "0.0") || 0.0,
|
||||
// Define how likely Replay events are sampled.
|
||||
// This sets the sample rate to be 10%. You may want this to be 100% while
|
||||
// in development and sample at a lower rate in production
|
||||
// replaysSessionSampleRate: parseFloat(process.env.SENTRY_REPLAYS_SESSION_SAMPLE_RATE ?? "0.0") || 0.0,
|
||||
|
||||
// Define how likely Replay events are sampled when an error occurs.
|
||||
// replaysOnErrorSampleRate: parseFloat(process.env.SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE ?? "0.0") || 0.0,
|
||||
// Define how likely Replay events are sampled when an error occurs.
|
||||
// replaysOnErrorSampleRate: parseFloat(process.env.SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE ?? "0.0") || 0.0,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: !!process.env.SENTRY_DEBUG,
|
||||
beforeSend(event) {
|
||||
if (
|
||||
event.exception?.values?.some(
|
||||
(e) =>
|
||||
// Ignore fake error "UnhandledRejection: Non-Error promise rejection captured with value: Object Not Found Matching Id:3, MethodName:update, ParamCount:4"
|
||||
// Raised GH issue: https://github.com/getsentry/sentry-javascript/issues/3440
|
||||
e.value?.includes("Non-Error promise rejection captured with") ||
|
||||
e.value?.includes("Object Not Found Matching Id")
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: !!process.env.SENTRY_DEBUG,
|
||||
beforeSend(event) {
|
||||
if (
|
||||
event.exception?.values?.some(
|
||||
(e) =>
|
||||
// Ignore fake error "UnhandledRejection: Non-Error promise rejection captured with value: Object Not Found Matching Id:3, MethodName:update, ParamCount:4"
|
||||
// Raised GH issue: https://github.com/getsentry/sentry-javascript/issues/3440
|
||||
e.value?.includes("Non-Error promise rejection captured with") ||
|
||||
e.value?.includes("Object Not Found Matching Id")
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
event.tags = {
|
||||
...event.tags,
|
||||
errorSource: "client",
|
||||
};
|
||||
return event;
|
||||
},
|
||||
});
|
||||
event.tags = {
|
||||
...event.tags,
|
||||
errorSource: "client",
|
||||
};
|
||||
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";
|
||||
|
||||
export async function register() {
|
||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "nodejs") {
|
||||
await import("./sentry.server.config");
|
||||
}
|
||||
|
||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "edge") {
|
||||
await import("./sentry.edge.config");
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "nodejs") {
|
||||
await import("./sentry.server.config");
|
||||
}
|
||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "edge") {
|
||||
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);
|
||||
}
|
||||
};
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 429 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user