Compare commits
132
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 | ||
|
|
e6959f3c7c | ||
|
|
67af8b4973 | ||
|
|
7794f2596e | ||
|
|
167cf6e352 | ||
|
|
3d778ba481 | ||
|
|
b8f0000b59 | ||
|
|
618ef63017 | ||
|
|
ae8e633716 | ||
|
|
4a23832388 | ||
|
|
02e486d757 | ||
|
|
af6e24f433 | ||
|
|
f9e24f51a7 | ||
|
|
9e9f815809 | ||
|
|
a91561f3c2 | ||
|
|
ff79064ca7 | ||
|
|
1a573e1619 | ||
|
|
511be7386c | ||
|
|
2a0991c259 | ||
|
|
2e454c9723 | ||
|
|
7e6d94c8e0 | ||
|
|
e18b790ccd | ||
|
|
0274417ad1 | ||
|
|
a8a3a93ace | ||
|
|
88bbab216e | ||
|
|
f74434a282 | ||
|
|
ed6f9d8252 | ||
|
|
a19ccab16a | ||
|
|
77369a13f1 | ||
|
|
8231356d2a | ||
|
|
469a942b5a | ||
|
|
0afe6cf5d1 | ||
|
|
75a161fe35 | ||
|
|
eb1ed107ba | ||
|
|
13e2e340ac | ||
|
|
0af62da16b | ||
|
|
9b323cbcc8 | ||
|
|
18a0e2d75c | ||
|
|
87c89a9968 | ||
|
|
e237749abd | ||
|
|
994a36d039 | ||
|
|
c989817373 |
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
-2
@@ -724,7 +724,7 @@ ${we.map(pe=>`Not found: ${lo(pe)}
|
||||
To understand the dependency tree requiring vulnerable packages, check the raw report with the \`--json\` flag or use \`yarn why <package>\` to get more information as to who depends on them.
|
||||
`,examples:[["Checks for known security issues with the installed packages. The output is a list of known issues.","yarn npm audit"],["Audit dependencies in all workspaces","yarn npm audit --all"],["Limit auditing to `dependencies` (excludes `devDependencies`)","yarn npm audit --environment production"],["Show audit report as valid JSON","yarn npm audit --json"],["Audit all direct and transitive dependencies","yarn npm audit --recursive"],["Output moderate (or more severe) vulnerabilities","yarn npm audit --severity moderate"],["Exclude certain packages","yarn npm audit --exclude package1 --exclude package2"],["Ignore specific advisories","yarn npm audit --ignore 1234567 --ignore 7654321"]]});var TM=Pe(Xr()),OM=J("util"),Yu=class extends De{constructor(){super(...arguments);this.fields=z.String("-f,--fields",{description:"A comma-separated list of manifest fields that should be displayed"});this.json=z.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.packages=z.Rest()}async execute(){let t=await ye.find(this.context.cwd,this.context.plugins),{project:i}=await je.find(t,this.context.cwd),n=typeof this.fields<"u"?new Set(["name",...this.fields.split(/\s*,\s*/)]):null,s=[],o=!1,a=await Ge.start({configuration:t,includeFooter:!1,json:this.json,stdout:this.context.stdout},async l=>{for(let c of this.packages){let u;if(c==="."){let L=i.topLevelWorkspace;if(!L.manifest.name)throw new be(`Missing ${ee.pretty(t,"name",ee.Type.CODE)} field in ${K.fromPortablePath(x.join(L.cwd,xt.manifest))}`);u=P.makeDescriptor(L.manifest.name,"unknown")}else u=P.parseDescriptor(c);let g=Mt.getIdentUrl(u),f=MM(await Mt.get(g,{configuration:t,ident:u,jsonResponse:!0,customErrorMessage:Mt.customPackageError})),h=Object.keys(f.versions).sort(TM.default.compareLoose),C=f["dist-tags"].latest||h[h.length-1],y=vt.validRange(u.range);if(y){let L=TM.default.maxSatisfying(h,y);L!==null?C=L:(l.reportWarning(0,`Unmet range ${P.prettyRange(t,u.range)}; falling back to the latest version`),o=!0)}else Object.prototype.hasOwnProperty.call(f["dist-tags"],u.range)?C=f["dist-tags"][u.range]:u.range!=="unknown"&&(l.reportWarning(0,`Unknown tag ${P.prettyRange(t,u.range)}; falling back to the latest version`),o=!0);let B=f.versions[C],v={...f,...B,version:C,versions:h},D;if(n!==null){D={};for(let L of n){let H=v[L];if(typeof H<"u")D[L]=H;else{l.reportWarning(1,`The ${ee.pretty(t,L,ee.Type.CODE)} field doesn't exist inside ${P.prettyIdent(t,u)}'s information`),o=!0;continue}}}else this.json||(delete v.dist,delete v.readme,delete v.users),D=v;l.reportJson(D),this.json||s.push(D)}});OM.inspect.styles.name="cyan";for(let l of s)(l!==s[0]||o)&&this.context.stdout.write(`
|
||||
`),this.context.stdout.write(`${(0,OM.inspect)(l,{depth:1/0,colors:!0,compact:!1})}
|
||||
`);return a.exitCode()}};Yu.paths=[["npm","info"]],Yu.usage=ve.Usage({category:"Npm-related commands",description:"show information about a package",details:"\n This command fetches information about a package from the npm registry and prints it in a tree format.\n\n The package does not have to be installed locally, but needs to have been published (in particular, local changes will be ignored even for workspaces).\n\n Append `@<range>` to the package argument to provide information specific to the latest version that satisfies the range or to the corresponding tagged version. If the range is invalid or if there is no version satisfying the range, the command will print a warning and fall back to the latest version.\n\n If the `-f,--fields` option is set, it's a comma-separated list of fields which will be used to only display part of the package information.\n\n By default, this command won't return the `dist`, `readme`, and `users` fields, since they are often very long. To explicitly request those fields, explicitly list them with the `--fields` flag or request the output in JSON mode.\n ",examples:[["Show all available information about react (except the `dist`, `readme`, and `users` fields)","yarn npm info react"],["Show all available information about react as valid JSON (including the `dist`, `readme`, and `users` fields)","yarn npm info react --json"],["Show all available information about react@16.12.0","yarn npm info react@16.12.0"],["Show all available information about react@next","yarn npm info react@next"],["Show the description of react","yarn npm info react --fields description"],["Show all available versions of react","yarn npm info react --fields versions"],["Show the readme of react","yarn npm info react --fields readme"],["Show a few fields of react","yarn npm info react --fields homepage,repository"]]});function MM(r){if(Array.isArray(r)){let e=[];for(let t of r)t=MM(t),t&&e.push(t);return e}else if(typeof r=="object"&&r!==null){let e={};for(let t of Object.keys(r)){if(t.startsWith("_"))continue;let i=MM(r[t]);i&&(e[t]=i)}return e}else return r||null}var sue=Pe(Pm()),ju=class extends De{constructor(){super(...arguments);this.scope=z.String("-s,--scope",{description:"Login to the registry configured for a given scope"});this.publish=z.Boolean("--publish",!1,{description:"Login to the publish registry"})}async execute(){let t=await ye.find(this.context.cwd,this.context.plugins),i=await Qb({configuration:t,cwd:this.context.cwd,publish:this.publish,scope:this.scope});return(await Ge.start({configuration:t,stdout:this.context.stdout,includeFooter:!1},async s=>{let o=await zVe({configuration:t,registry:i,report:s,stdin:this.context.stdin,stdout:this.context.stdout}),a=`/-/user/org.couchdb.user:${encodeURIComponent(o.name)}`,l=await Mt.put(a,o,{attemptedAs:o.name,configuration:t,registry:i,jsonResponse:!0,authType:Mt.AuthType.NO_AUTH});return await WVe(i,l.token,{configuration:t,scope:this.scope}),s.reportInfo(0,"Successfully logged in")})).exitCode()}};ju.paths=[["npm","login"]],ju.usage=ve.Usage({category:"Npm-related commands",description:"store new login info to access the npm registry",details:"\n This command will ask you for your username, password, and 2FA One-Time-Password (when it applies). It will then modify your local configuration (in your home folder, never in the project itself) to reference the new tokens thus generated.\n\n Adding the `-s,--scope` flag will cause the authentication to be done against whatever registry is configured for the associated scope (see also `npmScopes`).\n\n Adding the `--publish` flag will cause the authentication to be done against the registry used when publishing the package (see also `publishConfig.registry` and `npmPublishRegistry`).\n ",examples:[["Login to the default registry","yarn npm login"],["Login to the registry linked to the @my-scope registry","yarn npm login --scope my-scope"],["Login to the publish registry for the current package","yarn npm login --publish"]]});async function Qb({scope:r,publish:e,configuration:t,cwd:i}){return r&&e?or.getScopeRegistry(r,{configuration:t,type:or.RegistryType.PUBLISH_REGISTRY}):r?or.getScopeRegistry(r,{configuration:t}):e?or.getPublishRegistry((await Ph(t,i)).manifest,{configuration:t}):or.getDefaultRegistry({configuration:t})}async function WVe(r,e,{configuration:t,scope:i}){let n=o=>a=>{let l=Ie.isIndexableObject(a)?a:{},c=l[o],u=Ie.isIndexableObject(c)?c:{};return{...l,[o]:{...u,npmAuthToken:e}}},s=i?{npmScopes:n(i)}:{npmRegistries:n(r)};return await ye.updateHomeConfiguration(s)}async function zVe({configuration:r,registry:e,report:t,stdin:i,stdout:n}){t.reportInfo(0,`Logging in to ${ee.pretty(r,e,ee.Type.URL)}`);let s=!1;if(e.match(/^https:\/\/npm\.pkg\.github\.com(\/|$)/)&&(t.reportInfo(0,"You seem to be using the GitHub Package Registry. Tokens must be generated with the 'repo', 'write:packages', and 'read:packages' permissions."),s=!0),t.reporLine truncated
|
||||
`);return a.exitCode()}};Yu.paths=[["npm","info"]],Yu.usage=ve.Usage({category:"Npm-related commands",description:"show information about a package",details:"\n This command fetches information about a package from the npm registry and prints it in a tree format.\n\n The package does not have to be installed locally, but needs to have been published (in particular, local changes will be ignored even for workspaces).\n\n Append `@<range>` to the package argument to provide information specific to the latest version that satisfies the range or to the corresponding tagged version. If the range is invalid or if there is no version satisfying the range, the command will print a warning and fall back to the latest version.\n\n If the `-f,--fields` option is set, it's a comma-separated list of fields which will be used to only display part of the package information.\n\n By default, this command won't return the `dist`, `readme`, and `users` fields, since they are often very long. To explicitly request those fields, explicitly list them with the `--fields` flag or request the output in JSON mode.\n ",examples:[["Show all available information about react (except the `dist`, `readme`, and `users` fields)","yarn npm info react"],["Show all available information about react as valid JSON (including the `dist`, `readme`, and `users` fields)","yarn npm info react --json"],["Show all available information about react@16.12.0","yarn npm info react@16.12.0"],["Show all available information about react@next","yarn npm info react@next"],["Show the description of react","yarn npm info react --fields description"],["Show all available versions of react","yarn npm info react --fields versions"],["Show the readme of react","yarn npm info react --fields readme"],["Show a few fields of react","yarn npm info react --fields homepage,repository"]]});function MM(r){if(Array.isArray(r)){let e=[];for(let t of r)t=MM(t),t&&e.push(t);return e}else if(typeof r=="object"&&r!==null){let e={};for(let t of Object.keys(r)){if(t.startsWith("_"))continue;let i=MM(r[t]);i&&(e[t]=i)}return e}else return r||null}var sue=Pe(Pm()),ju=class extends De{constructor(){super(...arguments);this.scope=z.String("-s,--filter",{description:"Login to the registry configured for a given scope"});this.publish=z.Boolean("--publish",!1,{description:"Login to the publish registry"})}async execute(){let t=await ye.find(this.context.cwd,this.context.plugins),i=await Qb({configuration:t,cwd:this.context.cwd,publish:this.publish,scope:this.scope});return(await Ge.start({configuration:t,stdout:this.context.stdout,includeFooter:!1},async s=>{let o=await zVe({configuration:t,registry:i,report:s,stdin:this.context.stdin,stdout:this.context.stdout}),a=`/-/user/org.couchdb.user:${encodeURIComponent(o.name)}`,l=await Mt.put(a,o,{attemptedAs:o.name,configuration:t,registry:i,jsonResponse:!0,authType:Mt.AuthType.NO_AUTH});return await WVe(i,l.token,{configuration:t,scope:this.scope}),s.reportInfo(0,"Successfully logged in")})).exitCode()}};ju.paths=[["npm","login"]],ju.usage=ve.Usage({category:"Npm-related commands",description:"store new login info to access the npm registry",details:"\n This command will ask you for your username, password, and 2FA One-Time-Password (when it applies). It will then modify your local configuration (in your home folder, never in the project itself) to reference the new tokens thus generated.\n\n Adding the `-s,--filter` flag will cause the authentication to be done against whatever registry is configured for the associated scope (see also `npmScopes`).\n\n Adding the `--publish` flag will cause the authentication to be done against the registry used when publishing the package (see also `publishConfig.registry` and `npmPublishRegistry`).\n ",examples:[["Login to the default registry","yarn npm login"],["Login to the registry linked to the @my-scope registry","yarn npm login --filter my-scope"],["Login to the publish registry for the current package","yarn npm login --publish"]]});async function Qb({scope:r,publish:e,configuration:t,cwd:i}){return r&&e?or.getScopeRegistry(r,{configuration:t,type:or.RegistryType.PUBLISH_REGISTRY}):r?or.getScopeRegistry(r,{configuration:t}):e?or.getPublishRegistry((await Ph(t,i)).manifest,{configuration:t}):or.getDefaultRegistry({configuration:t})}async function WVe(r,e,{configuration:t,scope:i}){let n=o=>a=>{let l=Ie.isIndexableObject(a)?a:{},c=l[o],u=Ie.isIndexableObject(c)?c:{};return{...l,[o]:{...u,npmAuthToken:e}}},s=i?{npmScopes:n(i)}:{npmRegistries:n(r)};return await ye.updateHomeConfiguration(s)}async function zVe({configuration:r,registry:e,report:t,stdin:i,stdout:n}){t.reportInfo(0,`Logging in to ${ee.pretty(r,e,ee.Type.URL)}`);let s=!1;if(e.match(/^https:\/\/npm\.pkg\.github\.com(\/|$)/)&&(t.reportInfo(0,"You seem to be using the GitHub Package Registry. Tokens must be generated with the 'repo', 'write:packages', and 'read:packages' permissions."),s=!0),t.reLine truncated
|
||||
This command will list all tags of a package from the npm registry.
|
||||
|
||||
If the package is not specified, Yarn will default to the current workspace.
|
||||
@@ -732,7 +732,7 @@ ${we.map(pe=>`Not found: ${lo(pe)}
|
||||
This command will add a tag to the npm registry for a specific version of a package. If the tag already exists, it will be overwritten.
|
||||
`,examples:[["Add a `beta` tag for version `2.3.4-beta.4` of package `my-pkg`","yarn npm tag add my-pkg@2.3.4-beta.4 beta"]]});var Vu=class extends De{constructor(){super(...arguments);this.package=z.String();this.tag=z.String()}async execute(){if(this.tag==="latest")throw new be("The 'latest' tag cannot be removed.");let t=await ye.find(this.context.cwd,this.context.plugins),{project:i,workspace:n}=await je.find(t,this.context.cwd);if(!n)throw new ct(i.cwd,this.context.cwd);let s=P.parseIdent(this.package),o=or.getPublishRegistry(n.manifest,{configuration:t}),a=ee.pretty(t,this.tag,ee.Type.CODE),l=ee.pretty(t,s,ee.Type.IDENT),c=await IE(s,t);if(!Object.prototype.hasOwnProperty.call(c,this.tag))throw new be(`${a} is not a tag of package ${l}`);return(await Ge.start({configuration:t,stdout:this.context.stdout},async g=>{let f=`/-/package${Mt.getIdentUrl(s)}/dist-tags/${encodeURIComponent(this.tag)}`;await Mt.del(f,{configuration:t,registry:o,ident:s,jsonResponse:!0}),g.reportInfo(0,`Tag ${a} removed from package ${l}`)})).exitCode()}};Vu.paths=[["npm","tag","remove"]],Vu.usage=ve.Usage({category:"Npm-related commands",description:"remove a tag from a package",details:`
|
||||
This command will remove a tag from a package from the npm registry.
|
||||
`,examples:[["Remove the `beta` tag from package `my-pkg`","yarn npm tag remove my-pkg beta"]]});var Xu=class extends De{constructor(){super(...arguments);this.scope=z.String("-s,--scope",{description:"Print username for the registry configured for a given scope"});this.publish=z.Boolean("--publish",!1,{description:"Print username for the publish registry"})}async execute(){let t=await ye.find(this.context.cwd,this.context.plugins),i;return this.scope&&this.publish?i=or.getScopeRegistry(this.scope,{configuration:t,type:or.RegistryType.PUBLISH_REGISTRY}):this.scope?i=or.getScopeRegistry(this.scope,{configuration:t}):this.publish?i=or.getPublishRegistry((await Ph(t,this.context.cwd)).manifest,{configuration:t}):i=or.getDefaultRegistry({configuration:t}),(await Ge.start({configuration:t,stdout:this.context.stdout},async s=>{var a,l;let o;try{o=await Mt.get("/-/whoami",{configuration:t,registry:i,authType:Mt.AuthType.ALWAYS_AUTH,jsonResponse:!0,ident:this.scope?P.makeIdent(this.scope,""):void 0})}catch(c){if(((a=c.response)==null?void 0:a.statusCode)===401||((l=c.response)==null?void 0:l.statusCode)===403){s.reportError(41,"Authentication failed - your credentials may have expired");return}else throw c}s.reportInfo(0,o.username)})).exitCode()}};Xu.paths=[["npm","whoami"]],Xu.usage=ve.Usage({category:"Npm-related commands",description:"display the name of the authenticated user",details:"\n Print the username associated with the current authentication settings to the standard output.\n\n When using `-s,--scope`, the username printed will be the one that matches the authentication settings of the registry associated with the given scope (those settings can be overriden using the `npmRegistries` map, and the registry associated with the scope is configured via the `npmScopes` map).\n\n When using `--publish`, the registry we'll select will by default be the one used when publishing packages (`publishConfig.registry` or `npmPublishRegistry` if available, otherwise we'll fallback to the regular `npmRegistryServer`).\n ",examples:[["Print username for the default registry","yarn npm whoami"],["Print username for the registry on a given scope","yarn npm whoami --scope company"]]});var _Ve={configuration:{npmPublishAccess:{description:"Default access of the published packages",type:"STRING",default:null},npmAuditExcludePackages:{description:"Array of glob patterns of packages to exclude from npm audit",type:"STRING",default:[],isArray:!0},npmAuditIgnoreAdvisories:{description:"Array of glob patterns of advisory IDs to exclude from npm audit",type:"STRING",default:[],isArray:!0}},commands:[Gu,Yu,ju,qu,Ju,zu,Wu,Vu,Xu]},ZVe=_Ve;var zM={};ut(zM,{default:()=>h6e,patchUtils:()=>_u});var _u={};ut(_u,{applyPatchFile:()=>Sb,diffFolders:()=>JM,ensureUnpatchedDescriptor:()=>HM,extractPackageToDisk:()=>qM,extractPatchFlags:()=>hue,isParentRequired:()=>jM,loadPatchFiles:()=>QE,makeDescriptor:()=>GM,makeLocator:()=>YM,makePatchHash:()=>WM,parseDescriptor:()=>wE,parseLocator:()=>BE,parsePatchFile:()=>yE});var $Ve=/^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@.*/;function Ap(r){return x.relative(Oe.root,x.resolve(Oe.root,K.toPortablePath(r)))}function e6e(r){let e=r.trim().match($Ve);if(!e)throw new Error(`Bad header line: '${r}'`);return{original:{start:Math.max(Number(e[1]),1),length:Number(e[3]||1)},patched:{start:Math.max(Number(e[4]),1),length:Number(e[6]||1)}}}var t6e=420,r6e=493;var Aue=()=>({semverExclusivity:null,diffLineFromPath:null,diffLineToPath:null,oldMode:null,newMode:null,deletedFileMode:null,newFileMode:null,renameFrom:null,renameTo:null,beforeHash:null,afterHash:null,fromPath:null,toPath:null,hunks:null}),i6e=r=>({header:e6e(r),parts:[]}),n6e={["@"]:"header",["-"]:"deletion",["+"]:"insertion",[" "]:"context",["\\"]:"pragma",undefined:"context"};function s6e(r){let e=[],t=Aue(),i="parsing header",n=null,s=null;function o(){n&&(s&&(n.parts.push(s),s=null),t.hunks.push(n),n=null)}function a(){o(),e.push(t),t=Aue()}for(let l=0;l<r.length;l++){let c=r[l];if(i==="parsing header")if(c.startsWith("@@"))i="parsing hunks",t.hunks=[],l-=1;else if(c.startsWith("diff --git ")){t&&t.diffLineFromPath&&a();let u=c.match(/^diff --git a\/(.*?) b\/(.*?)\s*$/);if(!u)throw new Error(`Bad diff line: ${c}`);t.diffLineFromPath=u[1],t.diffLineToPath=u[2]}else if(c.startsWith("old mode "))t.oldMode=c.slice(9).trim();else if(c.startsWith("new mode "))t.newMode=c.slice(9).trim();else if(c.startsWith("deleted file mode "))t.deletedFileMode=c.slice(18).trim();else if(c.startsWith("new file mode "))t.newFileMode=c.slice(14).trim();else if(c.startsWith("rename from "))t.renameFrom=c.slice(12).trim();else if(c.startsWith("rename to "))t.renameTo=c.slice(10).trim();else if(c.startsWith("index ")){let u=c.match(/(\w+)\.\.(\w+)/);if(!u)continue;t.beforeHash=u[1],t.afterHash=u[2]}else c.startsWith("semver exclusivity ")?t.semverExclusivity=c.slice(19).trim():c.startsWith("--- ")?t.fromPath=c.slice(6).trim():c.startsWith("+++ ")&&(t.toP
Line truncated
|
||||
`,examples:[["Remove the `beta` tag from package `my-pkg`","yarn npm tag remove my-pkg beta"]]});var Xu=class extends De{constructor(){super(...arguments);this.scope=z.String("-s,--filter",{description:"Print username for the registry configured for a given scope"});this.publish=z.Boolean("--publish",!1,{description:"Print username for the publish registry"})}async execute(){let t=await ye.find(this.context.cwd,this.context.plugins),i;return this.scope&&this.publish?i=or.getScopeRegistry(this.scope,{configuration:t,type:or.RegistryType.PUBLISH_REGISTRY}):this.scope?i=or.getScopeRegistry(this.scope,{configuration:t}):this.publish?i=or.getPublishRegistry((await Ph(t,this.context.cwd)).manifest,{configuration:t}):i=or.getDefaultRegistry({configuration:t}),(await Ge.start({configuration:t,stdout:this.context.stdout},async s=>{var a,l;let o;try{o=await Mt.get("/-/whoami",{configuration:t,registry:i,authType:Mt.AuthType.ALWAYS_AUTH,jsonResponse:!0,ident:this.scope?P.makeIdent(this.scope,""):void 0})}catch(c){if(((a=c.response)==null?void 0:a.statusCode)===401||((l=c.response)==null?void 0:l.statusCode)===403){s.reportError(41,"Authentication failed - your credentials may have expired");return}else throw c}s.reportInfo(0,o.username)})).exitCode()}};Xu.paths=[["npm","whoami"]],Xu.usage=ve.Usage({category:"Npm-related commands",description:"display the name of the authenticated user",details:"\n Print the username associated with the current authentication settings to the standard output.\n\n When using `-s,--filter`, the username printed will be the one that matches the authentication settings of the registry associated with the given scope (those settings can be overriden using the `npmRegistries` map, and the registry associated with the scope is configured via the `npmScopes` map).\n\n When using `--publish`, the registry we'll select will by default be the one used when publishing packages (`publishConfig.registry` or `npmPublishRegistry` if available, otherwise we'll fallback to the regular `npmRegistryServer`).\n ",examples:[["Print username for the default registry","yarn npm whoami"],["Print username for the registry on a given scope","yarn npm whoami --filter company"]]});var _Ve={configuration:{npmPublishAccess:{description:"Default access of the published packages",type:"STRING",default:null},npmAuditExcludePackages:{description:"Array of glob patterns of packages to exclude from npm audit",type:"STRING",default:[],isArray:!0},npmAuditIgnoreAdvisories:{description:"Array of glob patterns of advisory IDs to exclude from npm audit",type:"STRING",default:[],isArray:!0}},commands:[Gu,Yu,ju,qu,Ju,zu,Wu,Vu,Xu]},ZVe=_Ve;var zM={};ut(zM,{default:()=>h6e,patchUtils:()=>_u});var _u={};ut(_u,{applyPatchFile:()=>Sb,diffFolders:()=>JM,ensureUnpatchedDescriptor:()=>HM,extractPackageToDisk:()=>qM,extractPatchFlags:()=>hue,isParentRequired:()=>jM,loadPatchFiles:()=>QE,makeDescriptor:()=>GM,makeLocator:()=>YM,makePatchHash:()=>WM,parseDescriptor:()=>wE,parseLocator:()=>BE,parsePatchFile:()=>yE});var $Ve=/^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@.*/;function Ap(r){return x.relative(Oe.root,x.resolve(Oe.root,K.toPortablePath(r)))}function e6e(r){let e=r.trim().match($Ve);if(!e)throw new Error(`Bad header line: '${r}'`);return{original:{start:Math.max(Number(e[1]),1),length:Number(e[3]||1)},patched:{start:Math.max(Number(e[4]),1),length:Number(e[6]||1)}}}var t6e=420,r6e=493;var Aue=()=>({semverExclusivity:null,diffLineFromPath:null,diffLineToPath:null,oldMode:null,newMode:null,deletedFileMode:null,newFileMode:null,renameFrom:null,renameTo:null,beforeHash:null,afterHash:null,fromPath:null,toPath:null,hunks:null}),i6e=r=>({header:e6e(r),parts:[]}),n6e={["@"]:"header",["-"]:"deletion",["+"]:"insertion",[" "]:"context",["\\"]:"pragma",undefined:"context"};function s6e(r){let e=[],t=Aue(),i="parsing header",n=null,s=null;function o(){n&&(s&&(n.parts.push(s),s=null),t.hunks.push(n),n=null)}function a(){o(),e.push(t),t=Aue()}for(let l=0;l<r.length;l++){let c=r[l];if(i==="parsing header")if(c.startsWith("@@"))i="parsing hunks",t.hunks=[],l-=1;else if(c.startsWith("diff --git ")){t&&t.diffLineFromPath&&a();let u=c.match(/^diff --git a\/(.*?) b\/(.*?)\s*$/);if(!u)throw new Error(`Bad diff line: ${c}`);t.diffLineFromPath=u[1],t.diffLineToPath=u[2]}else if(c.startsWith("old mode "))t.oldMode=c.slice(9).trim();else if(c.startsWith("new mode "))t.newMode=c.slice(9).trim();else if(c.startsWith("deleted file mode "))t.deletedFileMode=c.slice(18).trim();else if(c.startsWith("new file mode "))t.newFileMode=c.slice(14).trim();else if(c.startsWith("rename from "))t.renameFrom=c.slice(12).trim();else if(c.startsWith("rename to "))t.renameTo=c.slice(10).trim();else if(c.startsWith("index ")){let u=c.match(/(\w+)\.\.(\w+)/);if(!u)continue;t.beforeHash=u[1],t.afterHash=u[2]}else c.startsWith("semver exclusivity ")?t.semverExclusivity=c.slice(19).trim():c.startsWith("--- ")?t.fromPath=c.slice(6).trim():c.startsWith("+++ ")&&(t.
Line truncated
|
||||
`)+(n.hunk.parts[0].noNewlineAtEndOfFile?"":`
|
||||
`):"";await e.mkdirpPromise(x.dirname(n.path),{chmod:493,utimes:[xr.SAFE_TIME,xr.SAFE_TIME]}),await e.writeFilePromise(n.path,s,{mode:n.mode}),await e.utimesPromise(n.path,xr.SAFE_TIME,xr.SAFE_TIME)}break;case"patch":await cp(e,n.path,async()=>{await c6e(n,{baseFs:e,dryRun:t})});break;case"mode change":{let o=(await e.statPromise(n.path)).mode;if(lue(n.newMode)!==lue(o))continue;await cp(e,n.path,async()=>{await e.chmodPromise(n.path,n.newMode)})}break;default:Ie.assertNever(n);break}}function lue(r){return(r&64)>0}function cue(r){return r.replace(/\s+$/,"")}function l6e(r,e){return cue(r)===cue(e)}async function c6e({hunks:r,path:e},{baseFs:t,dryRun:i=!1}){let n=await t.statSync(e).mode,o=(await t.readFileSync(e,"utf8")).split(/\n/),a=[],l=0,c=0;for(let g of r){let f=Math.max(c,g.header.patched.start+l),h=Math.max(0,f-c),p=Math.max(0,o.length-f-g.header.original.length),C=Math.max(h,p),y=0,B=0,v=null;for(;y<=C;){if(y<=h&&(B=f-y,v=uue(g,o,B),v!==null)){y=-y;break}if(y<=p&&(B=f+y,v=uue(g,o,B),v!==null))break;y+=1}if(v===null)throw new lp(r.indexOf(g),g);a.push(v),l+=y,c=B+g.header.original.length}if(i)return;let u=0;for(let g of a)for(let f of g)switch(f.type){case"splice":{let h=f.index+u;o.splice(h,f.numToDelete,...f.linesToInsert),u+=f.linesToInsert.length-f.numToDelete}break;case"pop":o.pop();break;case"push":o.push(f.line);break;default:Ie.assertNever(f);break}await t.writeFilePromise(e,o.join(`
|
||||
`),{mode:n})}function uue(r,e,t){let i=[];for(let n of r.parts)switch(n.type){case"context":case"deletion":{for(let s of n.lines){let o=e[t];if(o==null||!l6e(o,s))return null;t+=1}n.type==="deletion"&&(i.push({type:"splice",index:t-n.lines.length,numToDelete:n.lines.length,linesToInsert:[]}),n.noNewlineAtEndOfFile&&i.push({type:"push",line:""}))}break;case"insertion":i.push({type:"splice",index:t,numToDelete:0,linesToInsert:n.lines}),n.noNewlineAtEndOfFile&&i.push({type:"pop"});break;default:Ie.assertNever(n.type);break}return i}var g6e=/^builtin<([^>]+)>$/;function UM(r,e){let{source:t,selector:i,params:n}=P.parseRange(r);if(t===null)throw new Error("Patch locators must explicitly define their source");let s=i?i.split(/&/).map(c=>K.toPortablePath(c)):[],o=n&&typeof n.locator=="string"?P.parseLocator(n.locator):null,a=n&&typeof n.version=="string"?n.version:null,l=e(t);return{parentLocator:o,sourceItem:l,patchPaths:s,sourceVersion:a}}function wE(r){let{sourceItem:e,...t}=UM(r.range,P.parseDescriptor);return{...t,sourceDescriptor:e}}function BE(r){let{sourceItem:e,...t}=UM(r.reference,P.parseLocator);return{...t,sourceLocator:e}}function HM(r){if(!r.range.startsWith("patch:"))return r;let{sourceItem:e}=UM(r.range,P.parseDescriptor);return e}function gue({parentLocator:r,sourceItem:e,patchPaths:t,sourceVersion:i,patchHash:n},s){let o=r!==null?{locator:P.stringifyLocator(r)}:{},a=typeof i<"u"?{version:i}:{},l=typeof n<"u"?{hash:n}:{};return P.makeRange({protocol:"patch:",source:s(e),selector:t.join("&"),params:{...a,...l,...o}})}function GM(r,{parentLocator:e,sourceDescriptor:t,patchPaths:i}){return P.makeDescriptor(r,gue({parentLocator:e,sourceItem:t,patchPaths:i},P.stringifyDescriptor))}function YM(r,{parentLocator:e,sourcePackage:t,patchPaths:i,patchHash:n}){return P.makeLocator(r,gue({parentLocator:e,sourceItem:t,sourceVersion:t.version,patchPaths:i,patchHash:n},P.stringifyLocator))}function fue({onAbsolute:r,onRelative:e,onBuiltin:t},i){i.startsWith("~")&&(i=i.slice(1));let s=i.match(g6e);return s!==null?t(s[1]):x.isAbsolute(i)?r(i):e(i)}function hue(r){let e=r.startsWith("~");return e&&(r=r.slice(1)),{optional:e}}function jM(r){return fue({onAbsolute:()=>!1,onRelative:()=>!0,onBuiltin:()=>!1},r)}async function QE(r,e,t){let i=r!==null?await t.fetcher.fetch(r,t):null,n=i&&i.localPath?{packageFs:new jt(Oe.root),prefixPath:x.relative(Oe.root,i.localPath)}:i;i&&i!==n&&i.releaseFs&&i.releaseFs();let s=await Ie.releaseAfterUseAsync(async()=>await Promise.all(e.map(async o=>{let a=hue(o),l=await fue({onAbsolute:async()=>await M.readFilePromise(o,"utf8"),onRelative:async()=>{if(n===null)throw new Error("Assertion failed: The parent locator should have been fetched");return await n.packageFs.readFilePromise(x.join(n.prefixPath,o),"utf8")},onBuiltin:async c=>await t.project.configuration.firstHook(u=>u.getBuiltinPatch,t.project,c)},o);return{...a,source:l}})));for(let o of s)typeof o.source=="string"&&(o.source=o.source.replace(/\r\n?/g,`
|
||||
|
||||
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
+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"
|
||||
@@ -199,7 +199,7 @@ There's some settings that you'll need to setup.
|
||||
Under Vercel > Your API Project > Settings
|
||||
|
||||
In General > Build & Development Settings
|
||||
BUILD COMMAND: `yarn turbo run build --scope=@calcom/api --include-dependencies --no-deps`
|
||||
BUILD COMMAND: `yarn turbo run build --filter=@calcom/api --no-deps`
|
||||
OUTPUT DIRECTORY: `apps/api/.next`
|
||||
|
||||
In Git > Ignored Build Step
|
||||
|
||||
@@ -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.306",
|
||||
"@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
+306
-4
@@ -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,
|
||||
@@ -15,11 +16,11 @@ import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.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 { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.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";
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
EventTypeOutput_2024_06_14,
|
||||
GetBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@@ -50,9 +52,9 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let schedulesRepositoryFixture: SchedulesRepositoryFixture;
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
|
||||
const platformAdminEmail = `managed-users-bookings-2024-08-13-admin-${randomString()}@api.com`;
|
||||
let platformAdmin: User;
|
||||
@@ -67,6 +69,10 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
let thirdManagedUser: CreateManagedUserData;
|
||||
|
||||
let firstManagedUserEventTypeId: number;
|
||||
let eventTypeRequiresConfirmationId: number;
|
||||
|
||||
const orgAdminManagedUserEmail = `managed-user-bookings-2024-08-13-org-admin-${randomString()}@api.com`;
|
||||
let orgAdminManagedUser: CreateManagedUserData;
|
||||
|
||||
let firstManagedUserBookingsCount = 0;
|
||||
let secondManagedUserBookingsCount = 0;
|
||||
@@ -75,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();
|
||||
@@ -85,9 +91,9 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
|
||||
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
|
||||
|
||||
@@ -95,6 +101,13 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
name: `oauth-client-users-organization-${randomString()}`,
|
||||
isPlatform: true,
|
||||
isOrganization: true,
|
||||
platformBilling: {
|
||||
create: {
|
||||
customerId: "cus_999",
|
||||
plan: "SCALE",
|
||||
subscriptionId: "sub_999",
|
||||
},
|
||||
},
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
@@ -243,6 +256,59 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
thirdManagedUser = responseBody.data;
|
||||
});
|
||||
|
||||
it(`should create org admin managed user`, async () => {
|
||||
const requestBody: CreateManagedUserInput = {
|
||||
email: orgAdminManagedUserEmail,
|
||||
timeZone: managedUsersTimeZone,
|
||||
weekStart: "Monday",
|
||||
timeFormat: 24,
|
||||
locale: Locales.FR,
|
||||
name: "Org Admin Smith",
|
||||
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.send(requestBody)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: CreateManagedUserOutput = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.email).toEqual(
|
||||
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
|
||||
);
|
||||
expect(responseBody.data.accessToken).toBeDefined();
|
||||
expect(responseBody.data.refreshToken).toBeDefined();
|
||||
|
||||
orgAdminManagedUser = responseBody.data;
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${organization.id}/memberships`)
|
||||
.set("x-cal-client-id", oAuthClient.id)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.send({
|
||||
userId: orgAdminManagedUser.user.id,
|
||||
role: "ADMIN",
|
||||
accepted: true,
|
||||
})
|
||||
.expect(201);
|
||||
});
|
||||
|
||||
it("should create an event type requiring confirmation for first managed user", async () => {
|
||||
const eventTypeRequiresConfirmation = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `managed-user-bookings-event-type-requires-confirmation-${randomString()}`,
|
||||
slug: `managed-user-bookings-event-type-requires-confirmation-${randomString()}`,
|
||||
length: 60,
|
||||
requiresConfirmation: true,
|
||||
},
|
||||
firstManagedUser.user.id
|
||||
);
|
||||
eventTypeRequiresConfirmationId = eventTypeRequiresConfirmation.id;
|
||||
});
|
||||
|
||||
describe("bookings using original emails", () => {
|
||||
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
@@ -543,10 +609,246 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("booking confirmation by org admin", () => {
|
||||
it("should allow org admin managed user to confirm booking using access token", async () => {
|
||||
const bookingRequiringConfirmation = await bookingsRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: firstManagedUser.user.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2030, 0, 9, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)),
|
||||
title: "Booking requiring confirmation",
|
||||
uid: `booking-requiring-confirmation-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeRequiresConfirmationId,
|
||||
},
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
status: "PENDING",
|
||||
responses: {
|
||||
name: secondManagedUser.user.name,
|
||||
email: secondManagedUserEmail,
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: secondManagedUserEmail,
|
||||
name: secondManagedUser.user.name!,
|
||||
locale: secondManagedUser.user.locale,
|
||||
timeZone: secondManagedUser.user.timeZone,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${bookingRequiringConfirmation.uid}/confirm`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
const bookingData = responseBody.data as BookingOutput_2024_08_13;
|
||||
expect(bookingData.status).toEqual("accepted");
|
||||
expect(bookingData.uid).toEqual(bookingRequiringConfirmation.uid);
|
||||
|
||||
const confirmedBooking = await bookingsRepositoryFixture.getByUid(bookingRequiringConfirmation.uid);
|
||||
expect(confirmedBooking?.status).toEqual("ACCEPTED");
|
||||
});
|
||||
|
||||
it("should allow org admin managed user to reject booking using access token", async () => {
|
||||
const bookingRequiringConfirmation = await bookingsRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: firstManagedUser.user.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2030, 0, 9, 15, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2030, 0, 9, 16, 0, 0)),
|
||||
title: "Booking requiring rejection",
|
||||
uid: `booking-requiring-rejection-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeRequiresConfirmationId,
|
||||
},
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
status: "PENDING",
|
||||
responses: {
|
||||
name: secondManagedUser.user.name,
|
||||
email: secondManagedUserEmail,
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: secondManagedUserEmail,
|
||||
name: secondManagedUser.user.name!,
|
||||
locale: secondManagedUser.user.locale,
|
||||
timeZone: secondManagedUser.user.timeZone,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${bookingRequiringConfirmation.uid}/decline`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
const bookingData = responseBody.data as BookingOutput_2024_08_13;
|
||||
expect(bookingData.status).toEqual("rejected");
|
||||
expect(bookingData.uid).toEqual(bookingRequiringConfirmation.uid);
|
||||
|
||||
const rejectedBooking = await bookingsRepositoryFixture.getByUid(bookingRequiringConfirmation.uid);
|
||||
expect(rejectedBooking?.status).toEqual("REJECTED");
|
||||
});
|
||||
|
||||
it("should return unauthorized when org admin tries to confirm regular user's booking", async () => {
|
||||
const regularUser = await userRepositoryFixture.create({
|
||||
email: `managed-user-bookings-2024-08-13-regular-user-${randomString()}@api.com`,
|
||||
name: "Regular User",
|
||||
});
|
||||
|
||||
const regularUserEventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `regular-user-event-type-requires-confirmation-${randomString()}`,
|
||||
slug: `regular-user-event-type-requires-confirmation-${randomString()}`,
|
||||
length: 60,
|
||||
requiresConfirmation: true,
|
||||
},
|
||||
regularUser.id
|
||||
);
|
||||
|
||||
const regularUserBooking = await bookingsRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: regularUser.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2030, 0, 9, 17, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2030, 0, 9, 18, 0, 0)),
|
||||
title: "Regular user booking requiring confirmation",
|
||||
uid: `regular-user-booking-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: regularUserEventType.id,
|
||||
},
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
status: "PENDING",
|
||||
responses: {
|
||||
name: "External Attendee",
|
||||
email: "external@example.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "external@example.com",
|
||||
name: "External Attendee",
|
||||
locale: "en",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${regularUserBooking.uid}/confirm`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
|
||||
.expect(401);
|
||||
|
||||
await userRepositoryFixture.delete(regularUser.id);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
await userRepositoryFixture.delete(thirdManagedUser.user.id);
|
||||
await userRepositoryFixture.delete(orgAdminManagedUser.user.id);
|
||||
await userRepositoryFixture.delete(platformAdmin.id);
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.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(),
|
||||
@@ -702,6 +705,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
beforeUpdatedDate: queryParams.beforeUpdatedAt,
|
||||
afterCreatedDate: queryParams.afterCreatedAt,
|
||||
beforeCreatedDate: queryParams.beforeCreatedAt,
|
||||
bookingUid: queryParams.bookingUid,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
+50
-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(
|
||||
@@ -2147,6 +2151,51 @@ describe("Event types Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("EventType Hidden Property", () => {
|
||||
let createdEventTypeId: number;
|
||||
|
||||
it("should create an event type with hidden=true", async () => {
|
||||
const createPayload = {
|
||||
title: "Hidden Event",
|
||||
slug: "hidden-event",
|
||||
lengthInMinutes: 30,
|
||||
hidden: true,
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(createPayload)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const createdEventType = responseBody.data;
|
||||
expect(createdEventType).toHaveProperty("id");
|
||||
expect(createdEventType.title).toEqual(createPayload.title);
|
||||
expect(createdEventType.slug).toEqual(createPayload.slug);
|
||||
expect(createdEventType.lengthInMinutes).toEqual(createPayload.lengthInMinutes);
|
||||
expect(createdEventType.hidden).toBe(true);
|
||||
|
||||
createdEventTypeId = createdEventType.id;
|
||||
});
|
||||
|
||||
it("should update the hidden property to false", async () => {
|
||||
const updatePayload = {
|
||||
hidden: false,
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.patch(`/api/v2/event-types/${createdEventTypeId}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(updatePayload)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const updatedEventType = responseBody.data;
|
||||
+expect(updatedEventType.hidden).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+6
@@ -89,6 +89,8 @@ type Input = Pick<
|
||||
| "hideCalendarEventDetails"
|
||||
| "hideOrganizerEmail"
|
||||
| "calVideoSettings"
|
||||
| "hidden"
|
||||
| "bookingRequiresAuthentication"
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
@@ -127,6 +129,8 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
hideCalendarEventDetails,
|
||||
hideOrganizerEmail,
|
||||
calVideoSettings,
|
||||
hidden,
|
||||
bookingRequiresAuthentication,
|
||||
} = databaseEventType;
|
||||
|
||||
const locations = this.transformLocations(databaseEventType.locations);
|
||||
@@ -203,6 +207,8 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
hideCalendarEventDetails,
|
||||
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: "",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -229,4 +229,19 @@ export class SchedulesRepository_2024_06_11 {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getSchedulesByUserIds(userIds: number[], skip: number, take: number) {
|
||||
return this.dbRead.prisma.schedule.findMany({
|
||||
where: {
|
||||
userId: {
|
||||
in: userIds,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
availability: true,
|
||||
},
|
||||
skip,
|
||||
take,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
|
||||
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
|
||||
import { PrismaMembershipRepository } from "@/lib/repositories/prisma-membership.repository";
|
||||
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
||||
import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository";
|
||||
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
|
||||
@@ -12,6 +13,7 @@ import { BusyTimesService } from "@/lib/services/busy-times.service";
|
||||
import { CacheService } from "@/lib/services/cache.service";
|
||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||
import { FilterHostsService } from "@/lib/services/filter-hosts.service";
|
||||
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
|
||||
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
||||
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
@@ -31,6 +33,7 @@ import { Module } from "@nestjs/common";
|
||||
PrismaTeamRepository,
|
||||
RedisService,
|
||||
PrismaFeaturesRepository,
|
||||
PrismaMembershipRepository,
|
||||
CheckBookingLimitsService,
|
||||
CacheService,
|
||||
AvailableSlotsService,
|
||||
@@ -38,6 +41,7 @@ import { Module } from "@nestjs/common";
|
||||
BusyTimesService,
|
||||
FilterHostsService,
|
||||
QualifiedHostsService,
|
||||
NoSlotsNotificationService,
|
||||
],
|
||||
exports: [AvailableSlotsService],
|
||||
})
|
||||
|
||||
@@ -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,12 @@
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { PrismaMembershipRepository as PrismaMembershipRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Injectable()
|
||||
export class PrismaMembershipRepository extends PrismaMembershipRepositoryLib {
|
||||
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||
super(dbWrite.prisma as unknown as PrismaClient);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository"
|
||||
import { BusyTimesService } from "@/lib/services/busy-times.service";
|
||||
import { CacheService } from "@/lib/services/cache.service";
|
||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
|
||||
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
@@ -35,7 +36,8 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
||||
checkBookingLimitsService: CheckBookingLimitsService,
|
||||
cacheService: CacheService,
|
||||
userAvailabilityService: UserAvailabilityService,
|
||||
busyTimesService: BusyTimesService
|
||||
busyTimesService: BusyTimesService,
|
||||
noSlotsNotificationService: NoSlotsNotificationService
|
||||
) {
|
||||
super({
|
||||
oooRepo: oooRepoDependency,
|
||||
@@ -53,6 +55,7 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
||||
busyTimesService,
|
||||
qualifiedHostsService,
|
||||
featuresRepo: featuresRepository,
|
||||
noSlotsNotificationService,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { PrismaMembershipRepository } from "@/lib/repositories/prisma-membership.repository";
|
||||
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { NoSlotsNotificationService as BaseNoSlotsNotificationService } from "@calcom/platform-libraries/slots";
|
||||
|
||||
@Injectable()
|
||||
export class NoSlotsNotificationService extends BaseNoSlotsNotificationService {
|
||||
constructor(
|
||||
teamRepository: PrismaTeamRepository,
|
||||
membershipRepository: PrismaMembershipRepository,
|
||||
redisService: RedisService
|
||||
) {
|
||||
super({
|
||||
teamRepo: teamRepository,
|
||||
membershipRepo: membershipRepository,
|
||||
redisClient: redisService,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/teams/
|
||||
import { OrganizationsUsersBookingsModule } from "@/modules/organizations/users/bookings/organizations-users-bookings.module";
|
||||
import { RouterModule } from "@/modules/router/router.module";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { TeamsSchedulesModule } from "@/modules/teams/schedules/teams-schedules.module";
|
||||
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
||||
import { VerifiedResourcesModule } from "@/modules/verified-resources/verified-resources.module";
|
||||
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||
@@ -38,6 +39,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
|
||||
OrganizationsRoutingFormsModule,
|
||||
VerifiedResourcesModule,
|
||||
RouterModule,
|
||||
TeamsSchedulesModule,
|
||||
],
|
||||
})
|
||||
export class EndpointsModule implements NestModule {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
+128
-1
@@ -66,6 +66,8 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
||||
let orgEventTypeId2: number;
|
||||
let nonOrgEventTypeId: number;
|
||||
|
||||
let collectiveOrgBookingUid: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
orgUserEmail2,
|
||||
@@ -91,12 +93,17 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await organizationsRepositoryFixture.create({ name: "organization bookings" });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
team1 = await teamRepositoryFixture.create({
|
||||
name: "team orgs booking 1",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: organization.id } },
|
||||
createdByOAuthClient: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
nonOrgUser1 = await userRepositoryFixture.create({
|
||||
email: nonOrgUserEmail1,
|
||||
@@ -315,6 +322,7 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
collectiveOrgBookingUid = data.uid;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts.length).toEqual(1);
|
||||
@@ -669,6 +677,125 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("get by bookingUid param", () => {
|
||||
it("should get a specific booking by bookingUid query param", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${organization.id}/bookings?bookingUid=${collectiveOrgBookingUid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(1);
|
||||
expect(responseDataIsBooking(data[0])).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(data[0])) {
|
||||
const booking: BookingOutput_2024_08_13 = data[0];
|
||||
expect(booking.uid).toEqual(collectiveOrgBookingUid);
|
||||
expect(booking.eventTypeId).toEqual(orgEventTypeId);
|
||||
expect(booking.status).toEqual("accepted");
|
||||
expect(booking.attendees.length).toEqual(2);
|
||||
expect(booking.attendees[0].name).toEqual("alice");
|
||||
expect(booking.attendees[0].email).toEqual("alice@gmail.com");
|
||||
} else {
|
||||
throw new Error("Expected single booking but received different response type");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should return empty array for non-existent booking UID query param", async () => {
|
||||
const nonExistentUid = "non-existent-booking-uid";
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${organization.id}/bookings?bookingUid=${nonExistentUid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return empty array for booking UID not belonging to the organization", async () => {
|
||||
const regularUser = await userRepositoryFixture.create({
|
||||
email: `org-bookings-regular-user-${Math.floor(Math.random() * 10000)}@api.com`,
|
||||
name: "Regular User",
|
||||
});
|
||||
|
||||
const regularUserEventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `regular-user-event-type-${Math.floor(Math.random() * 10000)}`,
|
||||
slug: `regular-user-event-type-${Math.floor(Math.random() * 10000)}`,
|
||||
length: 60,
|
||||
bookingFields: [],
|
||||
locations: [],
|
||||
},
|
||||
regularUser.id
|
||||
);
|
||||
|
||||
const regularUserBooking = await bookingsRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: regularUser.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2030, 0, 15, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2030, 0, 15, 14, 0, 0)),
|
||||
title: "Regular user booking",
|
||||
uid: `regular-user-booking-${Math.floor(Math.random() * 10000)}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: regularUserEventType.id,
|
||||
},
|
||||
},
|
||||
location: "https://meet.google.com/regular-user",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
status: "ACCEPTED",
|
||||
responses: {
|
||||
name: "Regular Attendee",
|
||||
email: "regular@example.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "regular@example.com",
|
||||
name: "Regular Attendee",
|
||||
locale: "en",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${organization.id}/bookings?bookingUid=${regularUserBooking.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.length).toEqual(0);
|
||||
|
||||
await userRepositoryFixture.delete(regularUser.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
|
||||
+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);
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@ type Input = Pick<
|
||||
| "hideOrganizerEmail"
|
||||
| "team"
|
||||
| "calVideoSettings"
|
||||
| "hidden"
|
||||
| "bookingRequiresAuthentication"
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
|
||||
+17
-1
@@ -2,6 +2,8 @@ import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inpu
|
||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
|
||||
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
||||
import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service";
|
||||
|
||||
@@ -62,10 +64,24 @@ export class OrganizationsMembershipService {
|
||||
}
|
||||
|
||||
async deleteOrgMembership(organizationId: number, membershipId: number) {
|
||||
const membership = await this.organizationsMembershipRepository.deleteOrgMembership(
|
||||
// Get the membership first to get the userId
|
||||
const membership = await this.organizationsMembershipRepository.findOrgMembership(
|
||||
organizationId,
|
||||
membershipId
|
||||
);
|
||||
|
||||
if (!membership) {
|
||||
throw new NotFoundException(
|
||||
`Membership with id ${membershipId} within organization id ${organizationId} not found`
|
||||
);
|
||||
}
|
||||
|
||||
await TeamService.removeMembers({
|
||||
teamIds: [organizationId],
|
||||
userIds: [membership.userId],
|
||||
isOrg: true,
|
||||
});
|
||||
|
||||
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { OrganizationsEventTypesPrivateLinksController } from "@/ee/event-types-private-links/controllers/organizations-event-types-private-links.controller";
|
||||
import { EventTypesPrivateLinksModule } from "@/ee/event-types-private-links/event-types-private-links.module";
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
|
||||
import { InputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/input-schedules.service";
|
||||
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository";
|
||||
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
|
||||
@@ -18,13 +22,10 @@ import { OrganizationAttributesService } from "@/modules/organizations/attribute
|
||||
import { OrganizationAttributeOptionRepository } from "@/modules/organizations/attributes/options/organization-attribute-options.repository";
|
||||
import { OrganizationsAttributesOptionsController } from "@/modules/organizations/attributes/options/organizations-attributes-options.controller";
|
||||
import { OrganizationAttributeOptionService } from "@/modules/organizations/attributes/options/services/organization-attributes-option.service";
|
||||
import { OrganizationsConferencingController } from "@/modules/organizations/conferencing/organizations-conferencing.controller";
|
||||
import { OrganizationsConferencingModule } from "@/modules/organizations/conferencing/organizations-conferencing.module";
|
||||
import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service";
|
||||
import { OrganizationsDelegationCredentialModule } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.module";
|
||||
import { OrganizationsEventTypesController } from "@/modules/organizations/event-types/organizations-event-types.controller";
|
||||
import { EventTypesPrivateLinksModule } from "@/ee/event-types-private-links/event-types-private-links.module";
|
||||
import { OrganizationsEventTypesPrivateLinksController } from "@/ee/event-types-private-links/controllers/organizations-event-types-private-links.controller";
|
||||
import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository";
|
||||
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer";
|
||||
import { InputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/input.service";
|
||||
@@ -38,7 +39,6 @@ import { OrganizationsMembershipOutputService } from "@/modules/organizations/me
|
||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||
import { OrganizationsOrganizationsModule } from "@/modules/organizations/organizations/organizations-organizations.module";
|
||||
import { OrganizationsSchedulesController } from "@/modules/organizations/schedules/organizations-schedules.controller";
|
||||
import { OrganizationSchedulesRepository } from "@/modules/organizations/schedules/organizations-schedules.repository";
|
||||
import { OrganizationsSchedulesService } from "@/modules/organizations/schedules/services/organizations-schedules.service";
|
||||
import { OrganizationsStripeModule } from "@/modules/organizations/stripe/organizations-stripe.module";
|
||||
import { OrganizationsStripeService } from "@/modules/organizations/stripe/services/organizations-stripe.service";
|
||||
@@ -65,6 +65,7 @@ import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
|
||||
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||
import { TeamsModule } from "@/modules/teams/teams/teams.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
@@ -102,7 +103,6 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsTeamsService,
|
||||
MembershipsRepository,
|
||||
OrganizationsSchedulesService,
|
||||
OrganizationSchedulesRepository,
|
||||
OrganizationsUsersRepository,
|
||||
OrganizationsUsersService,
|
||||
EmailService,
|
||||
@@ -144,6 +144,9 @@ import { Module } from "@nestjs/common";
|
||||
TeamWorkflowsService,
|
||||
WorkflowsInputService,
|
||||
WorkflowsOutputService,
|
||||
TeamsSchedulesService,
|
||||
SchedulesService_2024_06_11,
|
||||
InputSchedulesService_2024_06_11,
|
||||
],
|
||||
exports: [
|
||||
OrganizationsService,
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
||||
import { OrganizationSchedulesRepository } from "@/modules/organizations/schedules/organizations-schedules.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@@ -8,8 +8,8 @@ import { ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
@Injectable()
|
||||
export class OrganizationsSchedulesService {
|
||||
constructor(
|
||||
private readonly organizationSchedulesService: OrganizationSchedulesRepository,
|
||||
private readonly outputSchedulesService: OutputSchedulesService_2024_06_11,
|
||||
private readonly schedulesRepository: SchedulesRepository_2024_06_11,
|
||||
private readonly usersRepository: UsersRepository
|
||||
) {}
|
||||
|
||||
@@ -17,7 +17,7 @@ export class OrganizationsSchedulesService {
|
||||
const users = await this.usersRepository.getOrganizationUsers(organizationId);
|
||||
const usersIds = users.map((user) => user.id);
|
||||
|
||||
const schedules = await this.organizationSchedulesService.getSchedulesByUserIds(usersIds, skip, take);
|
||||
const schedules = await this.schedulesRepository.getSchedulesByUserIds(usersIds, skip, take);
|
||||
|
||||
const responseSchedules: ScheduleOutput_2024_06_11[] = [];
|
||||
|
||||
|
||||
-4
@@ -20,7 +20,6 @@ import {
|
||||
OrgTeamMembershipOutputResponseDto,
|
||||
} from "@/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output";
|
||||
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service";
|
||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
||||
import {
|
||||
Controller,
|
||||
@@ -59,7 +58,6 @@ export class OrganizationsTeamsMembershipsController {
|
||||
|
||||
constructor(
|
||||
private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService,
|
||||
private teamsEventTypesService: TeamsEventTypesService,
|
||||
private readonly organizationsRepository: OrganizationsRepository
|
||||
) {}
|
||||
|
||||
@@ -127,8 +125,6 @@ export class OrganizationsTeamsMembershipsController {
|
||||
membershipId
|
||||
);
|
||||
|
||||
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
||||
|
||||
+13
-1
@@ -3,6 +3,8 @@ import { UpdateOrgTeamMembershipDto } from "@/modules/organizations/teams/member
|
||||
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsTeamsMembershipsService {
|
||||
constructor(
|
||||
@@ -58,11 +60,21 @@ export class OrganizationsTeamsMembershipsService {
|
||||
}
|
||||
|
||||
async deleteOrgTeamMembership(organizationId: number, teamId: number, membershipId: number) {
|
||||
const teamMembership = await this.organizationsTeamsMembershipsRepository.deleteOrgTeamMembershipById(
|
||||
// First get the membership to get the userId
|
||||
const teamMembership = await this.organizationsTeamsMembershipsRepository.findOrgTeamMembership(
|
||||
organizationId,
|
||||
teamId,
|
||||
membershipId
|
||||
);
|
||||
|
||||
if (!teamMembership) {
|
||||
throw new NotFoundException(
|
||||
`Membership with id ${membershipId} not found in team ${teamId} of organization ${organizationId}`
|
||||
);
|
||||
}
|
||||
|
||||
await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false });
|
||||
|
||||
return teamMembership;
|
||||
}
|
||||
}
|
||||
+33
-3
@@ -14,11 +14,12 @@ import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||
import { IsUserInOrgTeam } from "@/modules/auth/guards/users/is-user-in-org-team.guard";
|
||||
import { Controller, UseGuards, Get, Param, ParseIntPipe } from "@nestjs/common";
|
||||
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query } from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
import { GetSchedulesOutput_2024_06_11, SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/organizations/:orgId/teams/:teamId",
|
||||
@@ -29,7 +30,33 @@ import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||
@ApiHeader(OPTIONAL_API_KEY_HEADER)
|
||||
export class OrganizationsTeamsSchedulesController {
|
||||
constructor(private schedulesService: SchedulesService_2024_06_11) {}
|
||||
constructor(
|
||||
private schedulesService: SchedulesService_2024_06_11,
|
||||
|
||||
private teamsSchedulesService: TeamsSchedulesService
|
||||
) {}
|
||||
|
||||
@UseGuards(IsTeamInOrg)
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@Get("/schedules")
|
||||
@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> {
|
||||
const { skip, take } = queryParams;
|
||||
|
||||
const schedules = await this.teamsSchedulesService.getTeamSchedules(teamId, skip, take);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: schedules,
|
||||
};
|
||||
}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@@ -40,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);
|
||||
|
||||
+39
@@ -18,6 +18,7 @@ import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
import { ApiSuccessResponse, ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
import { Team, Schedule } from "@calcom/prisma/client";
|
||||
|
||||
@@ -36,6 +37,8 @@ describe("Organizations Teams Schedules Endpoints", () => {
|
||||
let org: Team;
|
||||
let orgTeam: Team;
|
||||
let nonOrgTeam: Team;
|
||||
|
||||
let userSchedule: Schedule;
|
||||
let user2Schedule: Schedule;
|
||||
|
||||
const userEmail = `organizations-teams-schedules-admin-${randomString()}@api.com`;
|
||||
@@ -73,6 +76,16 @@ describe("Organizations Teams Schedules Endpoints", () => {
|
||||
username: userEmail2,
|
||||
});
|
||||
|
||||
userSchedule = await scheduleRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
name: `organizations-teams-schedules-user-schedule-${randomString()}`,
|
||||
timeZone: "America/New_York",
|
||||
});
|
||||
|
||||
user2Schedule = await scheduleRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
@@ -214,6 +227,32 @@ describe("Organizations Teams Schedules Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should get all the schedules of members in a team", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/schedules`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: GetSchedulesOutput_2024_06_11 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(Array.isArray(responseBody.data)).toBe(true);
|
||||
expect(responseBody.data.length).toEqual(2);
|
||||
|
||||
const userOneSchedule = responseBody.data.find((schedule) => schedule.id === userSchedule.id);
|
||||
const userTwoSchedule = responseBody.data.find((schedule) => schedule.id === user2Schedule.id);
|
||||
|
||||
expect(userOneSchedule).toBeDefined();
|
||||
expect(userTwoSchedule).toBeDefined();
|
||||
|
||||
expect(userOneSchedule?.id).toEqual(userSchedule.id);
|
||||
expect(userOneSchedule?.name).toEqual(userSchedule.name);
|
||||
expect(userOneSchedule?.timeZone).toEqual(userSchedule.timeZone);
|
||||
|
||||
expect(userTwoSchedule?.id).toEqual(user2Schedule.id);
|
||||
expect(userTwoSchedule?.name).toEqual(user2Schedule.name);
|
||||
expect(userOneSchedule?.timeZone).toEqual(user2Schedule.timeZone);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await userRepositoryFixture.deleteByEmail(userToInviteViaApi.email);
|
||||
|
||||
+21
-1
@@ -12,7 +12,12 @@ import {
|
||||
EMAIL,
|
||||
WorkflowEmailAttendeeStepDto,
|
||||
} from "@/modules/workflows/inputs/workflow-step.input";
|
||||
import { BEFORE_EVENT, DAY } from "@/modules/workflows/inputs/workflow-trigger.input";
|
||||
import {
|
||||
BEFORE_EVENT,
|
||||
DAY,
|
||||
OnAfterEventTriggerDto,
|
||||
OnBeforeEventTriggerDto,
|
||||
} from "@/modules/workflows/inputs/workflow-trigger.input";
|
||||
// Adjust path if needed
|
||||
import { GetWorkflowOutput, GetWorkflowsOutput } from "@/modules/workflows/outputs/workflow.output";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
@@ -255,6 +260,10 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
||||
"+37255555555"
|
||||
);
|
||||
expect(responseBody.data.steps.find((step) => step.stepNumber === 3)?.email).toEqual(authEmail);
|
||||
const trigger = sampleCreateWorkflowDto.trigger as OnBeforeEventTriggerDto;
|
||||
expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value);
|
||||
expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit);
|
||||
|
||||
createdWorkflowId = responseBody.data.id;
|
||||
createdWorkflow = responseBody.data;
|
||||
});
|
||||
@@ -330,6 +339,13 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
||||
expect(step1).toBeDefined();
|
||||
const partialUpdateDto: Partial<CreateWorkflowDto> = {
|
||||
name: updatedName,
|
||||
trigger: {
|
||||
type: "afterEvent",
|
||||
offset: {
|
||||
unit: "minute",
|
||||
value: 10,
|
||||
},
|
||||
},
|
||||
steps: step1 ? [{ ...step1, sender: "updatedSender" } as WorkflowEmailAttendeeStepDto] : [],
|
||||
};
|
||||
expect(createdWorkflowId).toBeDefined();
|
||||
@@ -348,6 +364,10 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
||||
step1 && expect(responseBody.data.steps[0].id).toEqual(step1.id);
|
||||
expect(responseBody.data.steps[0].sender).toEqual("updatedSender");
|
||||
expect(responseBody.data.steps[1]?.id).toBeUndefined();
|
||||
const trigger = partialUpdateDto.trigger as OnAfterEventTriggerDto;
|
||||
expect(responseBody.data.trigger?.type).toEqual(trigger.type);
|
||||
expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value);
|
||||
expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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`);
|
||||
@@ -156,17 +157,4 @@ export class TeamsEventTypesService {
|
||||
|
||||
return this.eventTypesRepository.deleteEventType(eventTypeId);
|
||||
}
|
||||
|
||||
async deleteUserTeamEventTypesAndHosts(userId: number, teamId: number) {
|
||||
try {
|
||||
await this.teamsEventTypesRepository.deleteUserManagedTeamEventTypes(userId, teamId);
|
||||
await this.teamsEventTypesRepository.removeUserFromTeamEventTypesHosts(userId, teamId);
|
||||
} catch (err) {
|
||||
this.logger.error("Could not remove user from all team event-types.", {
|
||||
error: err,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-8
@@ -3,7 +3,6 @@ import { API_KEY_HEADER } from "@/lib/docs/headers";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
||||
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
|
||||
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
|
||||
import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output";
|
||||
@@ -45,10 +44,7 @@ import { SkipTakePagination } from "@calcom/platform-types";
|
||||
export class TeamsMembershipsController {
|
||||
private logger = new Logger("TeamsMembershipsController");
|
||||
|
||||
constructor(
|
||||
private teamsMembershipsService: TeamsMembershipsService,
|
||||
private teamsEventTypesService: TeamsEventTypesService
|
||||
) {}
|
||||
constructor(private teamsMembershipsService: TeamsMembershipsService) {}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@Post("/")
|
||||
@@ -127,7 +123,6 @@ export class TeamsMembershipsController {
|
||||
membershipId,
|
||||
body
|
||||
);
|
||||
|
||||
if (!currentMembership.accepted && updatedMembership.accepted) {
|
||||
try {
|
||||
await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId);
|
||||
@@ -151,8 +146,6 @@ export class TeamsMembershipsController {
|
||||
): Promise<DeleteTeamMembershipOutput> {
|
||||
const membership = await this.teamsMembershipsService.deleteTeamMembership(teamId, membershipId);
|
||||
|
||||
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
||||
|
||||
@@ -3,6 +3,8 @@ import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/up
|
||||
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class TeamsMembershipsService {
|
||||
constructor(private readonly teamsMembershipsRepository: TeamsMembershipsRepository) {}
|
||||
@@ -41,10 +43,15 @@ export class TeamsMembershipsService {
|
||||
}
|
||||
|
||||
async deleteTeamMembership(teamId: number, membershipId: number) {
|
||||
const teamMembership = await this.teamsMembershipsRepository.deleteTeamMembershipById(
|
||||
teamId,
|
||||
membershipId
|
||||
);
|
||||
// First get the membership to get the userId
|
||||
const teamMembership = await this.teamsMembershipsRepository.findTeamMembership(teamId, membershipId);
|
||||
|
||||
if (!teamMembership) {
|
||||
throw new NotFoundException(`Membership with id ${membershipId} not found in team ${teamId}`);
|
||||
}
|
||||
|
||||
await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false });
|
||||
|
||||
return teamMembership;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query } from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { GetSchedulesOutput_2024_06_11, SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/teams/:teamId/schedules",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, RolesGuard)
|
||||
@DocsTags("Teams / Schedules")
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
export class TeamsSchedulesController {
|
||||
constructor(private teamsSchedulesService: TeamsSchedulesService) {}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@Get("/")
|
||||
@ApiOperation({
|
||||
summary: "Get all team member schedules",
|
||||
})
|
||||
async getTeamSchedules(
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@Query() queryParams: SkipTakePagination
|
||||
): Promise<GetSchedulesOutput_2024_06_11> {
|
||||
const { skip, take } = queryParams;
|
||||
|
||||
const schedules = await this.teamsSchedulesService.getTeamSchedules(teamId, skip, take);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: schedules,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.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";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
import { Team, Schedule } from "@calcom/prisma/client";
|
||||
|
||||
describe("Teams Schedules Endpoints", () => {
|
||||
describe("User Authentication - User is Team Admin", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
|
||||
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||
let scheduleRepositoryFixture: SchedulesRepositoryFixture;
|
||||
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
let nonOrgTeam: Team;
|
||||
|
||||
let userSchedule: Schedule;
|
||||
let user2Schedule: Schedule;
|
||||
|
||||
const userEmail = `teams-schedules-admin-${randomString()}@api.com`;
|
||||
const userEmail2 = `teams-schedules-member-${randomString()}@api.com`;
|
||||
|
||||
let user: User;
|
||||
let user2: User;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
})
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
|
||||
scheduleRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
});
|
||||
user2 = await userRepositoryFixture.create({
|
||||
email: userEmail2,
|
||||
username: userEmail2,
|
||||
});
|
||||
|
||||
userSchedule = await scheduleRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
name: `teams-schedules-user-schedule-${randomString()}`,
|
||||
timeZone: "America/New_York",
|
||||
});
|
||||
|
||||
user2Schedule = await scheduleRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: user2.id,
|
||||
},
|
||||
},
|
||||
name: `teams-schedules-user2-schedule-${randomString()}`,
|
||||
timeZone: "America/New_York",
|
||||
});
|
||||
|
||||
nonOrgTeam = await teamsRepositoryFixture.create({
|
||||
name: `teams-schedules-non-org-team-${randomString()}`,
|
||||
isOrganization: false,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "ADMIN",
|
||||
user: { connect: { id: user.id } },
|
||||
team: { connect: { id: nonOrgTeam.id } },
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: user2.id } },
|
||||
team: { connect: { id: nonOrgTeam.id } },
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("should get all the schedules of members in a team", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/teams/${nonOrgTeam.id}/schedules`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: GetSchedulesOutput_2024_06_11 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(Array.isArray(responseBody.data)).toBe(true);
|
||||
expect(responseBody.data.length).toEqual(2);
|
||||
|
||||
const userOneSchedule = responseBody.data.find((schedule) => schedule.id === userSchedule.id);
|
||||
const userTwoSchedule = responseBody.data.find((schedule) => schedule.id === user2Schedule.id);
|
||||
|
||||
expect(userOneSchedule).toBeDefined();
|
||||
expect(userTwoSchedule).toBeDefined();
|
||||
|
||||
expect(userOneSchedule?.id).toEqual(userSchedule.id);
|
||||
expect(userOneSchedule?.name).toEqual(userSchedule.name);
|
||||
expect(userOneSchedule?.timeZone).toEqual(userSchedule.timeZone);
|
||||
|
||||
expect(userTwoSchedule?.id).toEqual(user2Schedule.id);
|
||||
expect(userTwoSchedule?.name).toEqual(user2Schedule.name);
|
||||
expect(userOneSchedule?.timeZone).toEqual(user2Schedule.timeZone);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await userRepositoryFixture.deleteByEmail(user2.email);
|
||||
await teamsRepositoryFixture.delete(nonOrgTeam.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
||||
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
export class TeamsSchedulesService {
|
||||
constructor(
|
||||
private readonly teamsRepository: TeamsRepository,
|
||||
private readonly schedulesRepository: SchedulesRepository_2024_06_11,
|
||||
private readonly outputSchedulesService: OutputSchedulesService_2024_06_11
|
||||
) {}
|
||||
|
||||
async getTeamSchedules(teamId: number, skip = 0, take = 250) {
|
||||
const userIds = await this.teamsRepository.getTeamUsersIds(teamId);
|
||||
const schedules = await this.schedulesRepository.getSchedulesByUserIds(userIds, skip, take);
|
||||
|
||||
const responseSchedules: ScheduleOutput_2024_06_11[] = [];
|
||||
|
||||
for (const schedule of schedules) {
|
||||
responseSchedules.push(await this.outputSchedulesService.getResponseSchedule(schedule));
|
||||
}
|
||||
|
||||
return responseSchedules;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { OrganizationSchedulesRepository } from "@/modules/organizations/schedules/organizations-schedules.repository";
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import { TeamsMembershipsService } from "@/modules/teams/memberships/services/teams-memberships.service";
|
||||
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||
import { TeamsSchedulesController } from "@/modules/teams/schedules/controllers/teams-schedules.controller";
|
||||
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||
import { TeamsController } from "@/modules/teams/teams/controllers/teams.controller";
|
||||
import { TeamsService } from "@/modules/teams/teams/services/teams.service";
|
||||
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, MembershipsModule, RedisModule],
|
||||
providers: [
|
||||
TeamsRepository,
|
||||
TeamsService,
|
||||
TeamsMembershipsRepository,
|
||||
TeamsMembershipsService,
|
||||
OutputSchedulesService_2024_06_11,
|
||||
OrganizationSchedulesRepository,
|
||||
StripeService,
|
||||
UsersRepository,
|
||||
AppsRepository,
|
||||
CredentialsRepository,
|
||||
OrganizationsRepository,
|
||||
TeamsSchedulesService,
|
||||
OrganizationsTeamsRepository,
|
||||
SchedulesRepository_2024_06_11,
|
||||
],
|
||||
controllers: [TeamsController, TeamsSchedulesController],
|
||||
exports: [TeamsRepository],
|
||||
})
|
||||
export class TeamsSchedulesModule {}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { TimeUnit, WorkflowTriggerEvents } from "@prisma/client";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsNumber, ValidateNested } from "class-validator";
|
||||
import { IsIn, IsNumber, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
export const BEFORE_EVENT = "beforeEvent";
|
||||
export const EVENT_CANCELLED = "eventCancelled";
|
||||
@@ -68,6 +68,8 @@ export class WorkflowTriggerOffsetDto {
|
||||
value!: number;
|
||||
|
||||
@ApiProperty({ description: "Unit for the offset time", example: HOUR })
|
||||
@IsString()
|
||||
@IsIn(TIME_UNITS)
|
||||
unit!: TimeUnitType;
|
||||
}
|
||||
|
||||
@@ -75,6 +77,8 @@ export class BaseWorkflowTriggerDto {
|
||||
@ApiProperty({
|
||||
description: "Trigger type for the workflow",
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([WORKFLOW_TRIGGER_TYPES])
|
||||
type!: WorkflowTriggerType;
|
||||
}
|
||||
|
||||
@@ -82,6 +86,8 @@ export class OnCreationTriggerDto {
|
||||
@ApiProperty({
|
||||
description: "Trigger type for the workflow",
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([NEW_EVENT])
|
||||
type: typeof NEW_EVENT = NEW_EVENT;
|
||||
}
|
||||
|
||||
@@ -89,12 +95,16 @@ export class OnRescheduleTriggerDto {
|
||||
@ApiProperty({
|
||||
description: "Trigger type for the workflow",
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([RESCHEDULE_EVENT])
|
||||
type: typeof RESCHEDULE_EVENT = RESCHEDULE_EVENT;
|
||||
}
|
||||
export class OnCancelTriggerDto {
|
||||
@ApiProperty({
|
||||
description: "Trigger type for the workflow",
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([EVENT_CANCELLED])
|
||||
type: typeof EVENT_CANCELLED = EVENT_CANCELLED;
|
||||
}
|
||||
|
||||
@@ -113,6 +123,8 @@ export class OnBeforeEventTriggerDto extends TriggerOffsetDTO {
|
||||
description: "Trigger type for the workflow",
|
||||
example: BEFORE_EVENT,
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([BEFORE_EVENT])
|
||||
type: typeof BEFORE_EVENT = BEFORE_EVENT;
|
||||
}
|
||||
|
||||
@@ -121,6 +133,8 @@ export class OnAfterEventTriggerDto extends TriggerOffsetDTO {
|
||||
description: "Trigger type for the workflow",
|
||||
example: AFTER_EVENT,
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([AFTER_EVENT])
|
||||
type: typeof AFTER_EVENT = AFTER_EVENT;
|
||||
}
|
||||
|
||||
@@ -129,6 +143,8 @@ export class OnAfterCalVideoGuestsNoShowTriggerDto extends TriggerOffsetDTO {
|
||||
description: "Trigger type for the workflow",
|
||||
example: AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([AFTER_GUESTS_CAL_VIDEO_NO_SHOW])
|
||||
type: typeof AFTER_GUESTS_CAL_VIDEO_NO_SHOW = AFTER_GUESTS_CAL_VIDEO_NO_SHOW;
|
||||
}
|
||||
|
||||
@@ -137,5 +153,7 @@ export class OnAfterCalVideoHostsNoShowTriggerDto extends TriggerOffsetDTO {
|
||||
description: "Trigger type for the workflow",
|
||||
example: AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([AFTER_HOSTS_CAL_VIDEO_NO_SHOW])
|
||||
type: typeof AFTER_HOSTS_CAL_VIDEO_NO_SHOW = AFTER_HOSTS_CAL_VIDEO_NO_SHOW;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -59,4 +59,8 @@ export class EventTypesRepositoryFixture {
|
||||
async delete(eventTypeId: EventType["id"]) {
|
||||
return this.prismaWriteClient.eventType.delete({ where: { id: eventTypeId } });
|
||||
}
|
||||
|
||||
async findById(eventTypeId: EventType["id"]) {
|
||||
return this.prismaReadClient.eventType.findUnique({ where: { id: eventTypeId } });
|
||||
}
|
||||
}
|
||||
@@ -35,4 +35,8 @@ export class MembershipRepositoryFixture {
|
||||
await this.prismaWriteClient.user.update({ where: { id: user.id }, data: { organizationId: org.id } });
|
||||
return membership;
|
||||
}
|
||||
|
||||
async findById(membershipId: Membership["id"]) {
|
||||
return this.prismaReadClient.membership.findUnique({ where: { id: membershipId } });
|
||||
}
|
||||
}
|
||||
@@ -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,13 +1,17 @@
|
||||
import { createRouterCaller } from "app/_trpc/context";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } 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 { AppCategories } from "@calcom/prisma/enums";
|
||||
import { appsRouter } from "@calcom/trpc/server/routers/viewer/apps/_router";
|
||||
import { calendarsRouter } from "@calcom/trpc/server/routers/viewer/calendars/_router";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import InstalledApps from "~/apps/installed/[category]/installed-category-view";
|
||||
|
||||
const querySchema = z.object({
|
||||
@@ -31,16 +35,23 @@ const InstalledAppsWrapper = async ({ params }: PageProps) => {
|
||||
redirect("/apps/installed/calendar");
|
||||
}
|
||||
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
if (!session?.user?.id) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
const [calendarsCaller, appsCaller] = await Promise.all([
|
||||
createRouterCaller(calendarsRouter),
|
||||
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 (
|
||||
<InstalledApps
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
Loaded 100 of 530 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user