Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37c26793f5 | ||
|
|
956f81fab3 | ||
|
|
1dcd54aab7 | ||
|
|
68d3d9d964 | ||
|
|
717a26f223 | ||
|
|
6818806c5e | ||
|
|
dd7f108f08 | ||
|
|
7e4d9e26c6 | ||
|
|
a7352cf587 | ||
|
|
5f8db0557c | ||
|
|
88be45457f | ||
|
|
90a42c37fd | ||
|
|
cf7430d531 | ||
|
|
105a82f97d | ||
|
|
fdd3d11111 | ||
|
|
e7ea1884b7 | ||
|
|
c04402e7ee | ||
|
|
075d209b6f | ||
|
|
26e8582302 | ||
|
|
9f277c5179 | ||
|
|
21411b8f93 | ||
|
|
0c160de404 | ||
|
|
a0eca13c18 | ||
|
|
5cda123a4c | ||
|
|
6169b92e98 | ||
|
|
cc1efe1298 | ||
|
|
7635d338c0 | ||
|
|
03ebabd6b4 | ||
|
|
0ab1bd0b6c | ||
|
|
908e5e05f2 | ||
|
|
3490272815 | ||
|
|
02dab51543 | ||
|
|
358b873aa7 | ||
|
|
44aa26292d | ||
|
|
b8530e2d78 | ||
|
|
7f1b94ad19 | ||
|
|
801ba50221 | ||
|
|
74af23136c | ||
|
|
da9ee5c186 | ||
|
|
037cb8ee2a | ||
|
|
f0e10129a1 | ||
|
|
7c5731e893 | ||
|
|
1012f2f153 | ||
|
|
d6f6be7d50 | ||
|
|
9e10dfd77b | ||
|
|
82782fcf77 | ||
|
|
2477220d2d | ||
|
|
89fe869766 | ||
|
|
1f102bf3b4 | ||
|
|
7a0b8a9eba | ||
|
|
8043505f3a | ||
|
|
919fc3cdd5 | ||
|
|
d7e6334ccb | ||
|
|
06e5c9803a | ||
|
|
528b27f31e | ||
|
|
4282cd221c | ||
|
|
38738c1a3a | ||
|
|
b5dcc7a991 | ||
|
|
5ceda11a0b | ||
|
|
787828d0ca | ||
|
|
89230474a5 | ||
|
|
d7fc11df5e | ||
|
|
1ff5d57ded | ||
|
|
19641ffc05 | ||
|
|
ea80e8c8d4 | ||
|
|
cba0fa8551 | ||
|
|
530cb94f0b | ||
|
|
d4ef6e83cf | ||
|
|
cddd8d7d9d | ||
|
|
1d6959c4ac | ||
|
|
a3fc17bc75 | ||
|
|
ad4b5957fc | ||
|
|
a7cb71726a | ||
|
|
3e7a848769 | ||
|
|
e6e67848b3 | ||
|
|
bf42cd4de9 | ||
|
|
6af7f8b0db | ||
|
|
421d03eb1a | ||
|
|
9d39742802 | ||
|
|
5267f8c3c1 | ||
|
|
c6ec42e735 | ||
|
|
1325e8488f | ||
|
|
d50a7b9d00 | ||
|
|
452729f4ab | ||
|
|
0206b364ee | ||
|
|
86353191d2 | ||
|
|
38f96d97d3 | ||
|
|
e6d44ce620 | ||
|
|
9e1e50e90b | ||
|
|
a73b804d48 | ||
|
|
4407a1952c | ||
|
|
dc3742d322 | ||
|
|
8b4f675cee | ||
|
|
53a931da4e | ||
|
|
228fbaf72e | ||
|
|
0c53717d0f | ||
|
|
e239b67171 | ||
|
|
fd390f9d43 | ||
|
|
eae779b81c | ||
|
|
6d621e0b60 | ||
|
|
ed3de528f3 | ||
|
|
92f1baa320 | ||
|
|
984cd64083 | ||
|
|
db8c7942a9 | ||
|
|
4e5d4f67d5 | ||
|
|
83488f0a35 | ||
|
|
9a52938f64 | ||
|
|
e24e1d8769 | ||
|
|
ef4be48ea6 | ||
|
|
5c9ad93923 | ||
|
|
85fe40688e | ||
|
|
829edec0ea | ||
|
|
81a99ea0b2 | ||
|
|
799e354059 | ||
|
|
584c991172 | ||
|
|
47e976df96 | ||
|
|
b44efddc0f | ||
|
|
bc665cbeab | ||
|
|
02b1393ff4 | ||
|
|
52e27bab53 | ||
|
|
45278a2195 | ||
|
|
2e8b89c4ee | ||
|
|
7d17b443cf | ||
|
|
c6ab6b3d13 | ||
|
|
c741d8133e | ||
|
|
b01cb27c56 | ||
|
|
c48f2043f2 | ||
|
|
630eee4fab | ||
|
|
1ebf553181 | ||
|
|
e8a315ac4e | ||
|
|
c196ff1095 | ||
|
|
bf0fbe4888 | ||
|
|
eb6418c0d5 | ||
|
|
2df2868b20 | ||
|
|
7f48c7fdad | ||
|
|
2101051623 | ||
|
|
a241cb0bfc | ||
|
|
5417012cec | ||
|
|
8e818e0567 | ||
|
|
1fdd04215d | ||
|
|
0bf0749b29 | ||
|
|
4006143e3a | ||
|
|
c6ad767565 | ||
|
|
896f8dbd3d | ||
|
|
f1db27c0ad | ||
|
|
3a1e04bb42 | ||
|
|
ab37847ea1 | ||
|
|
0a2ddf05ca | ||
|
|
04fc0f1d52 | ||
|
|
b76c4c9dcc | ||
|
|
68b71bfb0a | ||
|
|
763c568797 | ||
|
|
4659d08c07 | ||
|
|
b9c350cc45 | ||
|
|
f63d705523 | ||
|
|
2907c5e696 | ||
|
|
dcb47e31ed | ||
|
|
cd5b75fe2c | ||
|
|
97bdf837a9 | ||
|
|
5663b2ae59 | ||
|
|
02b57eb82e | ||
|
|
b23dfa194e | ||
|
|
2484b6fad2 | ||
|
|
c154bb31b7 | ||
|
|
0cf4ebd544 | ||
|
|
805c4d4c09 | ||
|
|
99330b3117 | ||
|
|
cc90a62090 | ||
|
|
79bcd6dc30 | ||
|
|
c6daa61e5e | ||
|
|
b693b6898a | ||
|
|
fb55bc2dcd | ||
|
|
71bd140fe4 | ||
|
|
4c5608147c | ||
|
|
7d16da5c98 | ||
|
|
0ae6e92558 | ||
|
|
052a38e39b | ||
|
|
31da30b982 | ||
|
|
d149f8385e | ||
|
|
ea5a0bd5f0 | ||
|
|
67c9fd92e1 | ||
|
|
50be997753 | ||
|
|
f00c14d0c1 | ||
|
|
8d4dd0241a | ||
|
|
90f97c3c92 | ||
|
|
1407886612 | ||
|
|
139e47fe3b | ||
|
|
9d4ecf2baa | ||
|
|
9d1cf3e70e | ||
|
|
6beb279a27 | ||
|
|
69e4b12f52 | ||
|
|
b8098ed3ab | ||
|
|
db55b11034 | ||
|
|
2273b7ea24 | ||
|
|
0a90212980 | ||
|
|
2efcfab715 | ||
|
|
3c1dfe8b74 | ||
|
|
e47ddf9835 | ||
|
|
08c62b554e | ||
|
|
b0b94b91fe | ||
|
|
af61b6d341 | ||
|
|
28693aba96 | ||
|
|
2dc0e2a6f2 | ||
|
|
6cbe1c1cf1 | ||
|
|
1c2da58d2a | ||
|
|
7bfdaa3d64 | ||
|
|
c38d2afcf5 | ||
|
|
807848cce2 | ||
|
|
e9d409c0d5 | ||
|
|
0c43d60039 | ||
|
|
1173822344 | ||
|
|
a934cec51a | ||
|
|
257acc3f71 | ||
|
|
91063c41f8 | ||
|
|
1e1298fdbc | ||
|
|
95120e1ff4 | ||
|
|
ee00f0d52c | ||
|
|
246abe0dd9 | ||
|
|
14e845ea1f | ||
|
|
8ba65c4450 | ||
|
|
aff494f21c | ||
|
|
18ad7e348e | ||
|
|
78f3d76ae2 | ||
|
|
6f9e04c6d9 | ||
|
|
fdb0e5d004 | ||
|
|
268a142370 | ||
|
|
29a3892669 | ||
|
|
0223027958 | ||
|
|
d7df154059 | ||
|
|
51d78e6f9d | ||
|
|
d5894d9fbc | ||
|
|
8b0539028e | ||
|
|
c7600d8ead | ||
|
|
6f5b8ead75 | ||
|
|
fc159fb9ea | ||
|
|
b3297f2843 | ||
|
|
1dade81299 | ||
|
|
b2839830c8 | ||
|
|
fa884e8516 | ||
|
|
3418a25a85 | ||
|
|
89cc175d73 | ||
|
|
c7c8564b63 | ||
|
|
b620581d7a | ||
|
|
c0530cc91c | ||
|
|
f738b3bc0a | ||
|
|
0853b4ca35 | ||
|
|
8be05b1dc5 | ||
|
|
71515ad17c | ||
|
|
66234ffba9 | ||
|
|
66c5f810ea | ||
|
|
81d9d84b4a | ||
|
|
6ddc666827 | ||
|
|
01346abb85 | ||
|
|
3d9650c841 | ||
|
|
f8ede75dd8 | ||
|
|
435587f043 | ||
|
|
e5abe93940 | ||
|
|
c3a5158a53 | ||
|
|
a9e14b9816 | ||
|
|
e207f40611 | ||
|
|
4ca2076c72 | ||
|
|
959b803d84 | ||
|
|
7f6136004c | ||
|
|
bb79368b2d | ||
|
|
70825dd138 | ||
|
|
05c399db9c | ||
|
|
57776b829b | ||
|
|
7fd28c39f3 | ||
|
|
ff17aa79fc | ||
|
|
578be32936 | ||
|
|
b183239ff5 | ||
|
|
013e614397 | ||
|
|
6843ced3db | ||
|
|
7318015a6e | ||
|
|
fc4d0a6aa3 | ||
|
|
8e0cdf3671 | ||
|
|
fcd9480f9f | ||
|
|
09ddbe886f | ||
|
|
4ac8a51bc2 | ||
|
|
82951cd04e | ||
|
|
ae87fdcc9f | ||
|
|
35e29c4bee | ||
|
|
387a1704f3 | ||
|
|
3fb8c5bbc5 | ||
|
|
626585f5d0 | ||
|
|
4d3727db12 | ||
|
|
f6641d5173 | ||
|
|
f9b21206a2 | ||
|
|
96821c0026 | ||
|
|
3d5e4754af | ||
|
|
613e9f0788 | ||
|
|
1261247e94 | ||
|
|
2ad843ffbb | ||
|
|
6923b97cd2 | ||
|
|
e639237088 | ||
|
|
5bc1fbb8c2 | ||
|
|
e310931417 | ||
|
|
17944311c3 | ||
|
|
d82f87aa2a | ||
|
|
6aa21aceb8 | ||
|
|
870ad57a7e | ||
|
|
fddee71829 | ||
|
|
6f15a5ab0f | ||
|
|
1fd64c5d7b | ||
|
|
f8614f11ff | ||
|
|
5e5e77236a | ||
|
|
4cee53b07c | ||
|
|
c289fac008 | ||
|
|
68f9e86cdd | ||
|
|
5da754db65 | ||
|
|
8e481f2921 | ||
|
|
0e6203ac3c | ||
|
|
624d6d7001 | ||
|
|
ec03ab632e | ||
|
|
c81b8145b5 | ||
|
|
fa35cc5210 | ||
|
|
1eb6ff3e65 | ||
|
|
fc4ecf500a | ||
|
|
9d4522825b | ||
|
|
e91bf53d80 | ||
|
|
79b319c1dd | ||
|
|
9197a73b31 | ||
|
|
ec3656e646 | ||
|
|
ae4b807500 | ||
|
|
34a9872df1 | ||
|
|
91a480b83a | ||
|
|
db19bb3d51 | ||
|
|
9afdb1ad7e | ||
|
|
78c2a69389 | ||
|
|
5a483e75c9 | ||
|
|
bb9c4bb8f3 | ||
|
|
02fdc1361c | ||
|
|
573132198e | ||
|
|
9133bc5b16 | ||
|
|
94ae3a5a73 | ||
|
|
0757b00db7 | ||
|
|
3402f79912 | ||
|
|
56ef5d667c | ||
|
|
bb9d99c3e1 | ||
|
|
1aac14056e | ||
|
|
7af46cc7ab | ||
|
|
37cec89bf6 | ||
|
|
67843adab9 | ||
|
|
64297f027c | ||
|
|
9fe5ff7721 | ||
|
|
3076aca457 | ||
|
|
5a59bb86cd | ||
|
|
17ab088af8 | ||
|
|
b81a307bd9 | ||
|
|
b312955838 | ||
|
|
003aaaa18b | ||
|
|
3b19c064fd | ||
|
|
e26a57bf25 | ||
|
|
b5585b3974 | ||
|
|
947b05bf90 | ||
|
|
a0e5e03903 | ||
|
|
09ee39a3d8 | ||
|
|
243012e82f | ||
|
|
d96c807505 | ||
|
|
dac1069181 | ||
|
|
06d6c180d1 | ||
|
|
a2bee76da6 | ||
|
|
52ecea964d | ||
|
|
f9b294cfc2 | ||
|
|
e5f14c9316 | ||
|
|
bed76beb2f | ||
|
|
ff38d6c7db | ||
|
|
1dc20e3aec | ||
|
|
44a3a9eabb | ||
|
|
c0c52750b6 | ||
|
|
3bcaf0ec22 | ||
|
|
b881e025f6 | ||
|
|
35d140e0a7 | ||
|
|
5fd55cbfc3 | ||
|
|
18c546a171 | ||
|
|
b01ceb427d | ||
|
|
eae2711f21 | ||
|
|
fac956b761 | ||
|
|
b286244aab | ||
|
|
c46f824e3f | ||
|
|
dbe55f0804 | ||
|
|
3bcce02b92 | ||
|
|
2a23911a5b | ||
|
|
5aa24c8472 | ||
|
|
8277ce934b | ||
|
|
bb68cd73ef | ||
|
|
445b307972 | ||
|
|
d2d86a4079 | ||
|
|
7601cc2194 | ||
|
|
aa48e72c12 | ||
|
|
bec49ef417 | ||
|
|
c13aa69565 | ||
|
|
1470adb159 | ||
|
|
bf89991eef | ||
|
|
4dfab874e0 | ||
|
|
4303fb8726 | ||
|
|
64f811ed0b | ||
|
|
4d51873140 | ||
|
|
3c0fc2e88b | ||
|
|
c702da0334 | ||
|
|
92169eb0d2 | ||
|
|
3d74357e96 | ||
|
|
6593291d00 | ||
|
|
c48fb44a8d | ||
|
|
e7c908bc80 | ||
|
|
ad35e19f8e | ||
|
|
3fc8c6c84e | ||
|
|
da7aeb4a21 | ||
|
|
6197a2a5c5 | ||
|
|
de186b95c1 | ||
|
|
6b24513073 | ||
|
|
55ea18128a | ||
|
|
d5557fa512 | ||
|
|
9c97b2aa6d | ||
|
|
1e226a40d1 | ||
|
|
bbe46eaa7d | ||
|
|
7799b191ec | ||
|
|
f3269a3ddf | ||
|
|
a4be822636 | ||
|
|
7cd8dbf7d8 | ||
|
|
654a865007 | ||
|
|
409a0277fb | ||
|
|
7adab1f65f | ||
|
|
3b92c86582 | ||
|
|
de195a1c0f | ||
|
|
d7d2487f6d | ||
|
|
3e31945157 | ||
|
|
c11814c342 | ||
|
|
60e3244088 | ||
|
|
a1ac5f36f0 | ||
|
|
6f38d40613 | ||
|
|
6b7d22de3f | ||
|
|
2f763dceda | ||
|
|
96468c4083 | ||
|
|
858d52563f | ||
|
|
7505b3b271 | ||
|
|
758dc88b55 | ||
|
|
9be418dc65 | ||
|
|
2ac310456e | ||
|
|
ff264d6f7a | ||
|
|
8019c414e1 | ||
|
|
a3fbe3731e | ||
|
|
e3742dbc66 | ||
|
|
83bf717d6d | ||
|
|
7fcdaa1c46 | ||
|
|
a8154f9405 | ||
|
|
52a5afeba5 | ||
|
|
43cc40d7fe | ||
|
|
42a98f000e | ||
|
|
f003ef935b | ||
|
|
e2b2a184c8 | ||
|
|
094595c435 | ||
|
|
4118a86c0d | ||
|
|
928d6f3821 | ||
|
|
b2ade568c8 | ||
|
|
27820ce897 | ||
|
|
fea7e66996 | ||
|
|
474d1571ae | ||
|
|
2ef627b87e | ||
|
|
2dfb82b623 | ||
|
|
ccd7fdfd52 | ||
|
|
f92e59ed34 | ||
|
|
d29477349a | ||
|
|
259e480baa | ||
|
|
0055dd1142 | ||
|
|
61c44780f0 | ||
|
|
9710d3b15c | ||
|
|
04cefebd75 | ||
|
|
dbd927c002 | ||
|
|
e6b2116a2b | ||
|
|
855a02091f | ||
|
|
358434429e | ||
|
|
b00061d85e | ||
|
|
022f08c05e | ||
|
|
6ff5c8ce5f | ||
|
|
3be7fb9dd8 | ||
|
|
72dd970b6e | ||
|
|
f4d09ece43 | ||
|
|
239e6085ba | ||
|
|
b17eab3aeb | ||
|
|
752f7fcde0 | ||
|
|
f6b061f787 | ||
|
|
cc8a1290ee | ||
|
|
b7cf893169 | ||
|
|
1458cacdbb | ||
|
|
a48a1bd5d8 | ||
|
|
1acd0a14ba | ||
|
|
6c9ac7636b | ||
|
|
d261a23dbc | ||
|
|
8b7947a172 | ||
|
|
89789b1e3b | ||
|
|
43dd24c480 | ||
|
|
ae6b1d402a | ||
|
|
3054ad33b2 | ||
|
|
a4dbb1a90b | ||
|
|
689150d78a | ||
|
|
fba096f23d | ||
|
|
7d5a3391f1 | ||
|
|
dcef99bf49 | ||
|
|
805fcc22a5 | ||
|
|
c5b7da8ffd | ||
|
|
97f1f7b130 | ||
|
|
0ac460fce3 | ||
|
|
82dfca8272 | ||
|
|
d1c5576d4f | ||
|
|
7090e3ed60 | ||
|
|
9926fd2e79 | ||
|
|
7793cb20d1 | ||
|
|
8ed0fb6a07 | ||
|
|
f5a1222fc5 | ||
|
|
2b892296b0 | ||
|
|
f796802ed3 | ||
|
|
b2ac227877 | ||
|
|
f0aec4efdc | ||
|
|
8d7cdf9d3b | ||
|
|
2ed0380e4f | ||
|
|
752cfa6bf9 | ||
|
|
f11c4dd7ec | ||
|
|
386f60ca6d | ||
|
|
e98f279f64 | ||
|
|
fe190f20f3 | ||
|
|
96855ea391 | ||
|
|
57150d992e | ||
|
|
8b75bf347f | ||
|
|
788fbdbe5e | ||
|
|
b2239374bc | ||
|
|
77c61abef8 | ||
|
|
c2472cc05a | ||
|
|
608fe1c586 | ||
|
|
4413295b27 | ||
|
|
bcc433ba9a | ||
|
|
2ba571d230 | ||
|
|
4a0a1ef053 | ||
|
|
7e882eedf8 | ||
|
|
224e606439 | ||
|
|
14b3344a41 | ||
|
|
a538ba64d2 | ||
|
|
b3bfad7fea | ||
|
|
f80b45e36a | ||
|
|
f3d60b141f | ||
|
|
5463dc261c | ||
|
|
55df0e3c9d | ||
|
|
730aab3e3d | ||
|
|
63740c02c7 | ||
|
|
c5942bcfcd | ||
|
|
c4547c51a2 | ||
|
|
cf89046ede | ||
|
|
d5d44fb64b | ||
|
|
4bb092c345 | ||
|
|
e1f7e94e36 | ||
|
|
2e9d5f7df1 | ||
|
|
8c943e8817 | ||
|
|
56821553fe | ||
|
|
b512ceb217 | ||
|
|
cfeb18de3f | ||
|
|
b407be933d | ||
|
|
4e1920471d | ||
|
|
6ee752aed8 | ||
|
|
a1922efab5 | ||
|
|
76332a759b |
No files matched your search
+219
-1
@@ -111,7 +111,7 @@ The event types page UI components are located in `apps/web/modules/event-types/
|
|||||||
|
|
||||||
Changes to shared UI patterns (like tab layouts and button alignments) need to be checked across multiple views to maintain consistency:
|
Changes to shared UI patterns (like tab layouts and button alignments) need to be checked across multiple views to maintain consistency:
|
||||||
- Event types page layout: `apps/web/modules/event-types/views/event-types-listing-view.tsx`
|
- Event types page layout: `apps/web/modules/event-types/views/event-types-listing-view.tsx`
|
||||||
- Bookings page layout: `apps/web/modules/bookings/views/bookings-listing-view.tsx`
|
- Bookings page layout: `apps/web/modules/bookings/views/bookings-view.tsx`
|
||||||
- Common elements like tabs, search bars, and filter buttons should maintain consistent alignment across views
|
- Common elements like tabs, search bars, and filter buttons should maintain consistent alignment across views
|
||||||
|
|
||||||
## When working on workflow triggers or similar enum-based features in the Cal.com codebase
|
## When working on workflow triggers or similar enum-based features in the Cal.com codebase
|
||||||
@@ -306,6 +306,224 @@ import { Button } from "@calcom/ui/components/button";
|
|||||||
- **Tests**: Same as source file + `.test.ts` or `.spec.ts`
|
- **Tests**: Same as source file + `.test.ts` or `.spec.ts`
|
||||||
- **Avoid**: Dot-suffixes like `.service.ts`, `.repository.ts` (except for tests, types, specs)
|
- **Avoid**: Dot-suffixes like `.service.ts`, `.repository.ts` (except for tests, types, specs)
|
||||||
|
|
||||||
|
## Repository Method Conventions
|
||||||
|
|
||||||
|
Repositories should follow consistent naming and design patterns to promote reusability and maintainability.
|
||||||
|
|
||||||
|
### Method Naming Rules
|
||||||
|
|
||||||
|
**1. Don't include the repository's entity name in method names**
|
||||||
|
|
||||||
|
Method names should be concise and avoid redundancy since the repository class name already indicates the entity type.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Good - Concise method names
|
||||||
|
class BookingRepository {
|
||||||
|
findById(id: string) { ... }
|
||||||
|
findByUserId(userId: string) { ... }
|
||||||
|
create(data: BookingCreateInput) { ... }
|
||||||
|
delete(id: string) { ... }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Bad - Redundant entity name in methods
|
||||||
|
class BookingRepository {
|
||||||
|
findBookingById(id: string) { ... }
|
||||||
|
findBookingByUserId(userId: string) { ... }
|
||||||
|
createBooking(data: BookingCreateInput) { ... }
|
||||||
|
deleteBooking(id: string) { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Use `include` or similar keywords for methods that fetch relational data**
|
||||||
|
|
||||||
|
When a method retrieves additional related entities, make this explicit in the method name using keywords like `include`, `with`, or `andRelations`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Good - Clear indication of included relations
|
||||||
|
class EventTypeRepository {
|
||||||
|
findById(id: string) {
|
||||||
|
return prisma.eventType.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
findByIdIncludeHosts(id: string) {
|
||||||
|
return prisma.eventType.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
hosts: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
findByIdIncludeHostsAndSchedule(id: string) {
|
||||||
|
return prisma.eventType.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
hosts: true,
|
||||||
|
schedule: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Bad - Unclear what data is included
|
||||||
|
class EventTypeRepository {
|
||||||
|
findById(id: string) {
|
||||||
|
return prisma.eventType.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
hosts: true,
|
||||||
|
schedule: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
findByIdForReporting(id: string) {
|
||||||
|
return prisma.eventType.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
hosts: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Keep methods generic and reusable - avoid use-case-specific names**
|
||||||
|
|
||||||
|
Repository methods should be general-purpose and describe what data they return, not how or where it's used. This promotes code reuse across different features.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Good - Generic, reusable methods
|
||||||
|
class BookingRepository {
|
||||||
|
findByUserIdIncludeAttendees(userId: string) {
|
||||||
|
return prisma.booking.findMany({
|
||||||
|
where: { userId },
|
||||||
|
include: {
|
||||||
|
attendees: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
findByDateRangeIncludeEventType(startDate: Date, endDate: Date) {
|
||||||
|
return prisma.booking.findMany({
|
||||||
|
where: {
|
||||||
|
startTime: { gte: startDate },
|
||||||
|
endTime: { lte: endDate },
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
eventType: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Bad - Use-case-specific method names
|
||||||
|
class BookingRepository {
|
||||||
|
findBookingsForReporting(userId: string) {
|
||||||
|
return prisma.booking.findMany({
|
||||||
|
where: { userId },
|
||||||
|
include: {
|
||||||
|
attendees: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
findBookingsForDashboard(startDate: Date, endDate: Date) {
|
||||||
|
return prisma.booking.findMany({
|
||||||
|
where: {
|
||||||
|
startTime: { gte: startDate },
|
||||||
|
endTime: { lte: endDate },
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
eventType: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. No business logic in repositories**
|
||||||
|
|
||||||
|
Repositories should only handle data access. Business logic, validations, and complex transformations belong in the Service layer.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Good - Repository only handles data access
|
||||||
|
class BookingRepository {
|
||||||
|
findByIdIncludeAttendees(id: string) {
|
||||||
|
return prisma.booking.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
attendees: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus(id: string, status: BookingStatus) {
|
||||||
|
return prisma.booking.update({
|
||||||
|
where: { id },
|
||||||
|
data: { status },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BookingService {
|
||||||
|
async confirmBooking(bookingId: string) {
|
||||||
|
const booking = await this.bookingRepository.findByIdIncludeAttendees(bookingId);
|
||||||
|
|
||||||
|
if (!booking) {
|
||||||
|
throw new Error("Booking not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (booking.status !== "PENDING") {
|
||||||
|
throw new Error("Only pending bookings can be confirmed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Business logic: send confirmation emails
|
||||||
|
await this.emailService.sendConfirmationToAttendees(booking.attendees);
|
||||||
|
|
||||||
|
// Update status through repository
|
||||||
|
return this.bookingRepository.updateStatus(bookingId, "CONFIRMED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Bad - Business logic in repository
|
||||||
|
class BookingRepository {
|
||||||
|
async confirmBooking(bookingId: string) {
|
||||||
|
const booking = await prisma.booking.findUnique({
|
||||||
|
where: { id: bookingId },
|
||||||
|
include: {
|
||||||
|
attendees: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!booking) {
|
||||||
|
throw new Error("Booking not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (booking.status !== "PENDING") {
|
||||||
|
throw new Error("Only pending bookings can be confirmed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Business logic shouldn't be here
|
||||||
|
await sendEmailToAttendees(booking.attendees);
|
||||||
|
|
||||||
|
return prisma.booking.update({
|
||||||
|
where: { id: bookingId },
|
||||||
|
data: { status: "CONFIRMED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
- Method names should be concise: `findById` not `findBookingById`
|
||||||
|
- Use `include`/`with` keywords when fetching relations: `findByIdIncludeHosts`
|
||||||
|
- Keep methods generic and reusable: `findByUserIdIncludeAttendees` not `findBookingsForReporting`
|
||||||
|
- No business logic in repositories - that belongs in Services
|
||||||
|
|
||||||
## When using Day.js
|
## When using Day.js
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@calcom/atoms": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix Booker atom verify email dialog infinite re-render when entering verification code
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"@calcom/atoms": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
fix: EventTypeSettings Checkbox booking field label
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@calcom/atoms": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Refactor SettingsHeader to accept onBackButtonClick callback instead of using useRouter directly.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@calcom/atoms": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add defaultPhoneCountry prop to BookerPlatformWrapper with ISO 3166-1 alpha-2 type safety
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
**/node_modules
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
.next
|
||||||
|
**/.next
|
||||||
|
dist
|
||||||
|
**/dist
|
||||||
|
build
|
||||||
|
**/build
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage
|
||||||
|
**/coverage
|
||||||
|
test-results
|
||||||
|
**/test-results
|
||||||
|
.turbo
|
||||||
|
**/.turbo
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Examples and docs
|
||||||
|
docs
|
||||||
@@ -144,6 +144,12 @@ GOOGLE_WEBHOOK_TOKEN=
|
|||||||
# Optional URL to override for tunelling webhooks. Defaults to NEXT_PUBLIC_WEBAPP_URL.
|
# Optional URL to override for tunelling webhooks. Defaults to NEXT_PUBLIC_WEBAPP_URL.
|
||||||
GOOGLE_WEBHOOK_URL=
|
GOOGLE_WEBHOOK_URL=
|
||||||
|
|
||||||
|
# Token to verify incoming webhooks from Microsoft Calendar
|
||||||
|
MICROSOFT_WEBHOOK_TOKEN=
|
||||||
|
|
||||||
|
# Optional URL to override for tunelling webhooks. Defaults to NEXT_PUBLIC_WEBAPP_URL.
|
||||||
|
MICROSOFT_WEBHOOK_URL=
|
||||||
|
|
||||||
# Inbox to send user feedback
|
# Inbox to send user feedback
|
||||||
SEND_FEEDBACK_EMAIL=
|
SEND_FEEDBACK_EMAIL=
|
||||||
|
|
||||||
@@ -261,6 +267,9 @@ NEXT_PUBLIC_CLOUDFLARE_SITEKEY=
|
|||||||
NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER=
|
NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER=
|
||||||
CLOUDFLARE_TURNSTILE_SECRET=
|
CLOUDFLARE_TURNSTILE_SECRET=
|
||||||
|
|
||||||
|
# 0 = false, 1=true
|
||||||
|
NEXT_PUBLIC_VERCEL_USE_BOTID_IN_BOOKER=
|
||||||
|
|
||||||
# Set the following value to true if you wish to enable Team Impersonation
|
# Set the following value to true if you wish to enable Team Impersonation
|
||||||
NEXT_PUBLIC_TEAM_IMPERSONATION=false
|
NEXT_PUBLIC_TEAM_IMPERSONATION=false
|
||||||
|
|
||||||
@@ -473,3 +482,10 @@ DATABASE_CHUNK_SIZE=
|
|||||||
# Service Account Encryption Key for encrypting/decrypting service account keys
|
# Service Account Encryption Key for encrypting/decrypting service account keys
|
||||||
# You can use: `openssl rand -base64 24` to generate one
|
# You can use: `openssl rand -base64 24` to generate one
|
||||||
CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY=
|
CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY=
|
||||||
|
|
||||||
|
SALESFORCE_GRAPHQL_DELAY_MS=500
|
||||||
|
SALESFORCE_GRAPHQL_MAX_DELAY_MS=2000
|
||||||
|
SALESFORCE_GRAPHQL_MAX_RETRIES=3
|
||||||
|
|
||||||
|
# Force Node.js to use UTC as process timezone
|
||||||
|
TZ=UTC
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
node_modules
|
|
||||||
apps/api/v2/dist
|
|
||||||
packages/platform/**/dist/*
|
|
||||||
**/**/node_modules
|
|
||||||
**/**/.next
|
|
||||||
**/**/public
|
|
||||||
packages/prisma/zod
|
|
||||||
apps/web/public/embed
|
|
||||||
packages/ui/components/icon/dynamicIconImports.tsx
|
|
||||||
-115
@@ -1,115 +0,0 @@
|
|||||||
/** @type {import("eslint").Linter.Config} */
|
|
||||||
module.exports = {
|
|
||||||
extends: ["./packages/config/eslint-preset.js"],
|
|
||||||
plugins: ["import"],
|
|
||||||
rules: {
|
|
||||||
"import/no-cycle": ["warn", { maxDepth: Infinity }],
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
// WARN: features must not be imported by app-store or lib
|
|
||||||
{
|
|
||||||
files: ["packages/app-store/**/*.{ts,tsx,js,jsx}", "packages/lib/**/*.{ts,tsx,js,jsx}"],
|
|
||||||
rules: {
|
|
||||||
"no-restricted-imports": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: [
|
|
||||||
// Catch all relative paths into features
|
|
||||||
"**/features",
|
|
||||||
"**/features/*",
|
|
||||||
// Catch all alias imports
|
|
||||||
"@calcom/features",
|
|
||||||
"@calcom/features/*",
|
|
||||||
],
|
|
||||||
message: "Avoid importing @calcom/features from @calcom/app-store or @calcom/lib.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// WARN: lib must not import app-store or features
|
|
||||||
{
|
|
||||||
files: ["packages/lib/**/*.{ts,tsx,js,jsx}"],
|
|
||||||
rules: {
|
|
||||||
"no-restricted-imports": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: [
|
|
||||||
// Catch all relative paths into app-store
|
|
||||||
"**/app-store",
|
|
||||||
"**/app-store/*",
|
|
||||||
// Catch all relative paths into features
|
|
||||||
"**/features",
|
|
||||||
"**/features/*",
|
|
||||||
// Catch alias imports
|
|
||||||
"@calcom/app-store",
|
|
||||||
"@calcom/app-store/*",
|
|
||||||
"@calcom/features",
|
|
||||||
"@calcom/features/*",
|
|
||||||
],
|
|
||||||
message: "@calcom/lib should not import @calcom/app-store or @calcom/features.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// ERROR: app-store must not import trpc
|
|
||||||
{
|
|
||||||
files: ["packages/app-store/**/*.{ts,tsx,js,jsx}"],
|
|
||||||
rules: {
|
|
||||||
"no-restricted-imports": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: [
|
|
||||||
// Catch all relative paths into trpc
|
|
||||||
"**/trpc",
|
|
||||||
"**/trpc/*",
|
|
||||||
// Catch alias imports
|
|
||||||
"@calcom/trpc",
|
|
||||||
"@calcom/trpc/*",
|
|
||||||
"@trpc",
|
|
||||||
"@trpc/*",
|
|
||||||
],
|
|
||||||
message:
|
|
||||||
"@calcom/app-store must not import trpc. Move UI to apps/web/components/apps or introduce an API boundary.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// ERROR: prisma must not import `features` package
|
|
||||||
{
|
|
||||||
files: ["packages/prisma/**/*.{ts,tsx,js,jsx}"],
|
|
||||||
rules: {
|
|
||||||
"no-restricted-imports": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: [
|
|
||||||
// Catch all relative paths into features
|
|
||||||
"**/features",
|
|
||||||
"**/features/*",
|
|
||||||
// Catch all alias imports
|
|
||||||
"@calcom/features",
|
|
||||||
"@calcom/features/*",
|
|
||||||
],
|
|
||||||
message: "Avoid importing @calcom/features from @calcom/prisma.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
+6
-7
@@ -1,6 +1,5 @@
|
|||||||
/**/package.json @calcom/Foundation
|
/**/package.json @calcom/Foundation
|
||||||
/apps/api/**/* @calcom/Platform @calcom/Foundation
|
/apps/api/**/* @calcom/API
|
||||||
/apps/ui-playground/**/* @calcom/Consumer @calcom/Foundation
|
|
||||||
/apps/web/**/layout.tsx @calcom/Consumer @calcom/Foundation
|
/apps/web/**/layout.tsx @calcom/Consumer @calcom/Foundation
|
||||||
/apps/web/lib/csp.ts @calcom/Foundation
|
/apps/web/lib/csp.ts @calcom/Foundation
|
||||||
/apps/web/lib/buildNonce.ts @calcom/Foundation
|
/apps/web/lib/buildNonce.ts @calcom/Foundation
|
||||||
@@ -10,7 +9,6 @@
|
|||||||
/apps/web/lib/handleOrgRedirect.ts @calcom/Foundation
|
/apps/web/lib/handleOrgRedirect.ts @calcom/Foundation
|
||||||
/apps/web/middleware.ts @calcom/Foundation
|
/apps/web/middleware.ts @calcom/Foundation
|
||||||
/deploy/**/* @calcom/Foundation
|
/deploy/**/* @calcom/Foundation
|
||||||
/infra/**/* @calcom/Foundation
|
|
||||||
/packages/app-store/applecalendar/**/* @calcom/Foundation
|
/packages/app-store/applecalendar/**/* @calcom/Foundation
|
||||||
/packages/app-store/caldavcalendar/**/* @calcom/Foundation
|
/packages/app-store/caldavcalendar/**/* @calcom/Foundation
|
||||||
/packages/app-store/exchange2013calendar/**/* @calcom/Foundation
|
/packages/app-store/exchange2013calendar/**/* @calcom/Foundation
|
||||||
@@ -23,11 +21,12 @@
|
|||||||
/packages/app-store/office365calendar/**/* @calcom/Foundation
|
/packages/app-store/office365calendar/**/* @calcom/Foundation
|
||||||
/packages/app-store/zohocalendar/**/* @calcom/Foundation
|
/packages/app-store/zohocalendar/**/* @calcom/Foundation
|
||||||
/packages/embeds/**/* @calcom/Foundation
|
/packages/embeds/**/* @calcom/Foundation
|
||||||
|
/packages/features/auth/lib/next-auth-options.ts @calcom/Foundation
|
||||||
/packages/features/bookings/lib/**/* @calcom/Foundation
|
/packages/features/bookings/lib/**/* @calcom/Foundation
|
||||||
/packages/lib/getAggregatedAvailability.ts @calcom/Foundation
|
/packages/features/availability/lib/getAggregatedAvailability/getAggregatedAvailability.ts @calcom/Foundation
|
||||||
/packages/lib/getUserAvailability.ts @calcom/Foundation
|
/packages/features/availability/lib/getUserAvailability.ts @calcom/Foundation
|
||||||
/packages/lib/server/getLuckyUser.ts @calcom/Foundation
|
/packages/features/bookings/lib/getLuckyUser.ts @calcom/Foundation
|
||||||
/packages/lib/slots.ts @calcom/Foundation
|
/packages/features/schedules/lib/slots.ts @calcom/Foundation
|
||||||
/packages/platform/atoms/**/*WebWrapper.tsx @calcom/Consumer
|
/packages/platform/atoms/**/*WebWrapper.tsx @calcom/Consumer
|
||||||
/packages/platform/**/* @calcom/Platform @calcom/Foundation
|
/packages/platform/**/* @calcom/Platform @calcom/Foundation
|
||||||
/packages/prisma/**/* @calcom/Foundation
|
/packages/prisma/**/* @calcom/Foundation
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ runs:
|
|||||||
with:
|
with:
|
||||||
path: ${{ inputs.path }}
|
path: ${{ inputs.path }}
|
||||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}
|
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}
|
||||||
- run: echo ${{ env.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }} && yarn db-seed
|
- run: yarn db-seed
|
||||||
if: steps.cache-db.outputs.cache-hit != 'true'
|
if: steps.cache-db.outputs.cache-hit != 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
- name: Postgres Dump Backup
|
- name: Postgres Dump Backup
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
name: "Docker Build and Test"
|
||||||
|
description: "Reusable action to build and test Docker images for Cal.com"
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
platform:
|
||||||
|
description: "Target platform (linux/amd64 or arm64)"
|
||||||
|
required: true
|
||||||
|
platform-suffix:
|
||||||
|
description: "Suffix to add to image tags (e.g., -arm)"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
dockerhub-username:
|
||||||
|
description: "Docker Hub username"
|
||||||
|
required: true
|
||||||
|
dockerhub-token:
|
||||||
|
description: "Docker Hub token"
|
||||||
|
required: true
|
||||||
|
github-token:
|
||||||
|
description: "GitHub token for GHCR"
|
||||||
|
required: true
|
||||||
|
postgres-user:
|
||||||
|
description: "PostgreSQL user"
|
||||||
|
required: true
|
||||||
|
postgres-password:
|
||||||
|
description: "PostgreSQL password"
|
||||||
|
required: true
|
||||||
|
postgres-db:
|
||||||
|
description: "PostgreSQL database name"
|
||||||
|
required: true
|
||||||
|
database-host:
|
||||||
|
description: "Database host"
|
||||||
|
required: true
|
||||||
|
push-image:
|
||||||
|
description: "Whether to push the built image"
|
||||||
|
required: false
|
||||||
|
default: "false"
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Log in to the Docker Hub registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ inputs.dockerhub-username }}
|
||||||
|
password: ${{ inputs.dockerhub-token }}
|
||||||
|
logout: true
|
||||||
|
|
||||||
|
- name: Log in to the Github Container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ inputs.github-token }}
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
docker.io/calendso/calendso
|
||||||
|
docker.io/calcom/cal.com
|
||||||
|
ghcr.io/calcom/cal.com
|
||||||
|
flavor: |
|
||||||
|
latest=${{ !github.event.release.prerelease }}
|
||||||
|
suffix=${{ inputs.platform-suffix }}
|
||||||
|
|
||||||
|
- name: Copy env
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
grep -o '^[^#]*' .env.example > .env
|
||||||
|
cat .env >> $GITHUB_ENV
|
||||||
|
echo "DATABASE_HOST=localhost:5432" >> $GITHUB_ENV
|
||||||
|
eval $(sed -e '/^#/d' -e 's/^/export /' -e 's/$/;/' .env) ;
|
||||||
|
|
||||||
|
- name: Start database
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
docker compose up -d database
|
||||||
|
|
||||||
|
- name: Show database logs and container status
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "--- Container Status ---"
|
||||||
|
docker compose ps database
|
||||||
|
|
||||||
|
echo "--- Container Logs ---"
|
||||||
|
docker compose logs database
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
driver-opts: |
|
||||||
|
network=container:database
|
||||||
|
buildkitd-flags: |
|
||||||
|
--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host
|
||||||
|
|
||||||
|
- name: Build image
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: ./
|
||||||
|
file: ./Dockerfile
|
||||||
|
load: true
|
||||||
|
push: false
|
||||||
|
platforms: ${{ inputs.platform }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
build-args: |
|
||||||
|
NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
|
||||||
|
NEXT_PUBLIC_API_V2_URL=http://localhost:5555/api/v2
|
||||||
|
NEXT_PUBLIC_LICENSE_CONSENT=agree
|
||||||
|
DATABASE_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@${{ inputs.database-host }}/${{ inputs.postgres-db }}
|
||||||
|
DATABASE_DIRECT_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@${{ inputs.database-host }}/${{ inputs.postgres-db }}
|
||||||
|
|
||||||
|
- name: Test runtime
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
tags="${{ steps.meta.outputs.tags }}"
|
||||||
|
IFS=',' read -ra ADDR <<< "$tags"
|
||||||
|
tag=${ADDR[0]}
|
||||||
|
|
||||||
|
docker run --rm --network stack \
|
||||||
|
-p 3000:3000 \
|
||||||
|
-e DATABASE_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@database/${{ inputs.postgres-db }} \
|
||||||
|
-e DATABASE_DIRECT_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@database/${{ inputs.postgres-db }} \
|
||||||
|
-e NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }} \
|
||||||
|
-e CALENDSO_ENCRYPTION_KEY=${{ env.CALENDSO_ENCRYPTION_KEY }} \
|
||||||
|
$tag &
|
||||||
|
|
||||||
|
server_pid=$!
|
||||||
|
|
||||||
|
echo "Waiting for the server to start..."
|
||||||
|
sleep 120
|
||||||
|
|
||||||
|
echo http://localhost:3000/auth/login
|
||||||
|
|
||||||
|
for i in {1..60}; do
|
||||||
|
echo "Checking server health ($i/60)..."
|
||||||
|
response=$(curl -o /dev/null -s -w "%{http_code}" http://localhost:3000/auth/login)
|
||||||
|
echo "HTTP Status Code: $response"
|
||||||
|
if [[ "$response" == "200" ]] || [[ "$response" == "307" ]]; then
|
||||||
|
echo "Server is healthy"
|
||||||
|
kill $server_pid
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Server health check failed"
|
||||||
|
kill $server_pid
|
||||||
|
exit 1
|
||||||
|
env:
|
||||||
|
NEXTAUTH_SECRET: "EI4qqDpcfdvf4A+0aQEEx8JjHxHSy4uWiZw/F32K+pA="
|
||||||
|
CALENDSO_ENCRYPTION_KEY: "0zfLtY99wjeLnsM7qsa8xsT+Q0oSgnOL"
|
||||||
|
|
||||||
|
- name: Push image
|
||||||
|
id: docker_push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
if: ${{ inputs.push-image == 'true' && !github.event.release.prerelease }}
|
||||||
|
with:
|
||||||
|
context: ./
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
platforms: ${{ inputs.platform }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
build-args: |
|
||||||
|
NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
|
||||||
|
NEXT_PUBLIC_API_V2_URL=http://localhost:5555/api/v2
|
||||||
|
NEXT_PUBLIC_LICENSE_CONSENT=agree
|
||||||
|
DATABASE_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@${{ inputs.database-host }}/${{ inputs.postgres-db }}
|
||||||
|
DATABASE_DIRECT_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@${{ inputs.database-host }}/${{ inputs.postgres-db }}
|
||||||
|
|
||||||
|
- name: Image digest
|
||||||
|
shell: bash
|
||||||
|
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
shell: bash
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
docker compose down
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
GET /v2/event-types/{eventTypeId} added to the 'data' response property 'oneOf' list for the response status '200'
|
||||||
|
GET /v2/event-types/{eventTypeId} added '#/components/schemas/EventTypeOutput_2024_06_14, #/components/schemas/TeamEventTypeOutput_2024_06_14' to the 'data' response property 'oneOf' list for the response status '200'
|
||||||
@@ -74,14 +74,8 @@ jobs:
|
|||||||
uses: ./.github/workflows/e2e-embed-react.yml
|
uses: ./.github/workflows/e2e-embed-react.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
e2e-atoms:
|
|
||||||
name: Tests
|
|
||||||
needs: [lint, build, build-atoms, build-api-v2]
|
|
||||||
uses: ./.github/workflows/e2e-atoms.yml
|
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
required:
|
required:
|
||||||
needs: [lint, type-check, unit-test, integration-test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store, e2e-atoms]
|
needs: [lint, type-check, unit-test, integration-test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ jobs:
|
|||||||
if: steps.cache-api-v2-build.outputs.cache-hit != 'true'
|
if: steps.cache-api-v2-build.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||||
yarn workspace @calcom/api-v2 run generate-schemas
|
yarn workspace @calcom/api-v2 run generate-schemas &&
|
||||||
rm -rf apps/api/v2/node_modules
|
rm -rf apps/api/v2/node_modules &&
|
||||||
yarn install
|
yarn install &&
|
||||||
yarn workspace @calcom/api-v2 run build
|
yarn workspace @calcom/api-v2 run build
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -6,23 +6,31 @@ on:
|
|||||||
- main
|
- main
|
||||||
|
|
||||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||||
permissions: {} # remove permissions and set explicitly under "permissions:" below
|
permissions:
|
||||||
|
id-token: write # for NPM provenance - developers can verify where and how it was built
|
||||||
|
contents: write # to create release by changesets/action below
|
||||||
|
pull-requests: write # to create pull request by changesets/action below
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
if: github.repository == 'calcom/cal.com' # prevent this action from running on forks
|
if: github.repository == 'calcom/cal.com' # prevent this action from running on forks
|
||||||
permissions:
|
|
||||||
id-token: write # for NPM provenance - developers can verify where and how it was built
|
|
||||||
contents: write # to create release by changesets/action below
|
|
||||||
pull-requests: write # to create pull request by changesets/action below
|
|
||||||
name: Release
|
name: Release
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
- name: Setup Node and registry for OIDC
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20.17.0"
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
always-auth: false
|
||||||
- name: Setup node, yarn and install dependencies
|
- name: Setup node, yarn and install dependencies
|
||||||
uses: ./.github/actions/yarn-install
|
uses: ./.github/actions/yarn-install
|
||||||
|
- name: Install npm at version supporting OIDC
|
||||||
|
run: npm install -g npm@11.5.1
|
||||||
- name: Create Release Pull Request or Publish to npm
|
- name: Create Release Pull Request or Publish to npm
|
||||||
id: changesets
|
id: changesets
|
||||||
uses: changesets/action@v1
|
uses: changesets/action@v1
|
||||||
@@ -33,8 +41,7 @@ jobs:
|
|||||||
title: "chore: version packages" # PR title
|
title: "chore: version packages" # PR title
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_CONFIG_PROVENANCE: "true"
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
VITE_BOOKER_EMBED_OAUTH_CLIENT_ID: ${{ secrets.VITE_BOOKER_EMBED_OAUTH_CLIENT_ID }}
|
VITE_BOOKER_EMBED_OAUTH_CLIENT_ID: ${{ secrets.VITE_BOOKER_EMBED_OAUTH_CLIENT_ID }}
|
||||||
VITE_BOOKER_EMBED_API_URL: ${{ secrets.VITE_BOOKER_EMBED_API_URL }}
|
VITE_BOOKER_EMBED_API_URL: ${{ secrets.VITE_BOOKER_EMBED_API_URL }}
|
||||||
NEXT_PUBLIC_WEBAPP_URL: ${{ secrets.NEXT_PUBLIC_WEBAPP_URL }}
|
NEXT_PUBLIC_WEBAPP_URL: ${{ secrets.NEXT_PUBLIC_WEBAPP_URL }}
|
||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
- name: Checkout GitHub Pages Branch
|
- name: Checkout GitHub Pages Branch
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: calcom/test-results
|
repository: calcom/test-results-2
|
||||||
ref: gh-pages
|
ref: gh-pages
|
||||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
- name: Checkout GitHub Pages Branch
|
- name: Checkout GitHub Pages Branch
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
repository: calcom/test-results
|
repository: calcom/test-results-2
|
||||||
ref: gh-pages
|
ref: gh-pages
|
||||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
- name: Set Git User
|
- name: Set Git User
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ jobs:
|
|||||||
days-before-close: -1
|
days-before-close: -1
|
||||||
days-before-issue-stale: 60
|
days-before-issue-stale: 60
|
||||||
days-before-issue-close: -1
|
days-before-issue-close: -1
|
||||||
days-before-pr-stale: 14
|
days-before-pr-stale: 7
|
||||||
days-before-pr-close: -1
|
days-before-pr-close: 14
|
||||||
stale-pr-message: "This PR is being marked as stale due to inactivity."
|
stale-pr-message: "This PR has been marked as stale due to inactivity. If you're still working on it or need any help, please let us know or update the PR to keep it active."
|
||||||
close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued."
|
close-pr-message: "This PR has been closed due to inactivity. Please feel free to reopen it if you'd like to continue the work."
|
||||||
operations-per-run: 100
|
operations-per-run: 100
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: E2E
|
name: Check breaking changes and run E2E
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
env:
|
env:
|
||||||
@@ -23,6 +23,10 @@ env:
|
|||||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||||
SLOTS_CACHE_TTL: ${{ secrets.CI_SLOTS_CACHE_TTL }}
|
SLOTS_CACHE_TTL: ${{ secrets.CI_SLOTS_CACHE_TTL }}
|
||||||
|
NEXT_PUBLIC_VAPID_PUBLIC_KEY: ${{ secrets.NEXT_PUBLIC_VAPID_PUBLIC_KEY }}
|
||||||
|
VAPID_PRIVATE_KEY: ${{ secrets.VAPID_PRIVATE_KEY }}
|
||||||
|
JWT_SECRET: ${{ secrets.CI_JWT_SECRET }}
|
||||||
|
NODE_ENV: ${{ vars.CI_NODE_ENV }}
|
||||||
jobs:
|
jobs:
|
||||||
e2e:
|
e2e:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
@@ -68,6 +72,22 @@ jobs:
|
|||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/cache-db
|
- uses: ./.github/actions/cache-db
|
||||||
|
|
||||||
|
- name: Generate Swagger
|
||||||
|
working-directory: apps/api/v2
|
||||||
|
run: yarn generate-swagger
|
||||||
|
|
||||||
|
- name: Check breaking changes
|
||||||
|
run: |
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD:/work" -w /work \
|
||||||
|
tufin/oasdiff:latest \
|
||||||
|
breaking \
|
||||||
|
https://raw.githubusercontent.com/calcom/cal.com/refs/heads/main/docs/api-reference/v2/openapi.json \
|
||||||
|
docs/api-reference/v2/openapi.json \
|
||||||
|
--fail-on ERR \
|
||||||
|
--err-ignore .github/oasdiff-err-ignore.txt
|
||||||
|
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
working-directory: apps/api/v2
|
working-directory: apps/api/v2
|
||||||
run: |
|
run: |
|
||||||
@@ -75,6 +95,7 @@ jobs:
|
|||||||
EXIT_CODE=$?
|
EXIT_CODE=$?
|
||||||
echo "yarn workspace @calcom/platform-libraries build && yarn test:e2e command exit code: $EXIT_CODE"
|
echo "yarn workspace @calcom/platform-libraries build && yarn test:e2e command exit code: $EXIT_CODE"
|
||||||
exit $EXIT_CODE
|
exit $EXIT_CODE
|
||||||
|
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: --max-old-space-size=4096
|
NODE_OPTIONS: --max-old-space-size=4096
|
||||||
|
INTEGRATION_TESTS: "true"
|
||||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
||||||
organization-name: calcom
|
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, self-hosted, teams, webhooks, workflows, zapier"
|
ignore-labels: "admin, app-store, ai, api, 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, sre, teams, webhooks, workflows, zapier"
|
||||||
apply-labels-from-issue:
|
apply-labels-from-issue:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ env:
|
|||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
|
||||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||||
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
|
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
|
||||||
|
|||||||
@@ -195,13 +195,6 @@ jobs:
|
|||||||
uses: ./.github/workflows/e2e-embed-react.yml
|
uses: ./.github/workflows/e2e-embed-react.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
e2e-atoms:
|
|
||||||
name: Tests
|
|
||||||
needs: [changes, check-label, build, build-atoms, build-api-v2]
|
|
||||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
|
||||||
uses: ./.github/workflows/e2e-atoms.yml
|
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze Build
|
name: Analyze Build
|
||||||
needs: [build]
|
needs: [build]
|
||||||
@@ -211,7 +204,7 @@ jobs:
|
|||||||
merge-reports:
|
merge-reports:
|
||||||
name: Merge reports
|
name: Merge reports
|
||||||
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
needs: [changes, check-label, e2e, e2e-embed, e2e-embed-react, e2e-app-store, e2e-atoms]
|
needs: [changes, check-label, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||||
uses: ./.github/workflows/merge-reports.yml
|
uses: ./.github/workflows/merge-reports.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
@@ -255,8 +248,7 @@ jobs:
|
|||||||
e2e-api-v2,
|
e2e-api-v2,
|
||||||
e2e-embed,
|
e2e-embed,
|
||||||
e2e-embed-react,
|
e2e-embed-react,
|
||||||
e2e-app-store,
|
e2e-app-store
|
||||||
e2e-atoms,
|
|
||||||
]
|
]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ env:
|
|||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
|
||||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||||
@@ -36,7 +35,6 @@ env:
|
|||||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build Web App
|
name: Build Web App
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- name: Checkout GitHub Pages Branch
|
- name: Checkout GitHub Pages Branch
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: calcom/test-results
|
repository: calcom/test-results-2
|
||||||
ref: gh-pages
|
ref: gh-pages
|
||||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
- name: Set Git User
|
- name: Set Git User
|
||||||
@@ -67,7 +67,7 @@ jobs:
|
|||||||
- name: Output Report URL as Workflow Annotation
|
- name: Output Report URL as Workflow Annotation
|
||||||
id: url
|
id: url
|
||||||
run: |
|
run: |
|
||||||
FULL_HTML_REPORT_URL=https://calcom.github.io/test-results/$HTML_REPORT_URL_PATH
|
FULL_HTML_REPORT_URL=https://calcom.github.io/test-results-2/$HTML_REPORT_URL_PATH
|
||||||
echo "::notice title=📋 Published Playwright Test Report::$FULL_HTML_REPORT_URL"
|
echo "::notice title=📋 Published Playwright Test Report::$FULL_HTML_REPORT_URL"
|
||||||
echo "link=$FULL_HTML_REPORT_URL" >> $GITHUB_OUTPUT
|
echo "link=$FULL_HTML_REPORT_URL" >> $GITHUB_OUTPUT
|
||||||
- name: Find Comment
|
- name: Find Comment
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
name: "Release Docker"
|
name: "Release Docker"
|
||||||
|
|
||||||
on: # yamllint disable-line rule:truthy
|
env:
|
||||||
release:
|
POSTGRES_USER: "unicorn_user"
|
||||||
types:
|
POSTGRES_PASSWORD: "magical_password"
|
||||||
- created
|
POSTGRES_DB: "calendso"
|
||||||
- published
|
DATABASE_HOST: "database:5432"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
# in case manual trigger is needed
|
# in case manual trigger is needed
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
@@ -12,11 +17,9 @@ on: # yamllint disable-line rule:truthy
|
|||||||
description: "v{Major}.{Minor}.{Patch}"
|
description: "v{Major}.{Minor}.{Patch}"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release-amd64:
|
||||||
name: "Remote Release"
|
name: "Release AMD64"
|
||||||
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
runs-on: "ubuntu-latest"
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -24,25 +27,84 @@ jobs:
|
|||||||
- name: "Determine tag"
|
- name: "Determine tag"
|
||||||
run: 'echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV'
|
run: 'echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV'
|
||||||
|
|
||||||
- name: "Run remote release workflow"
|
- name: Build and test Docker image
|
||||||
uses: "actions/github-script@v6"
|
uses: ./.github/actions/docker-build-and-test
|
||||||
with:
|
with:
|
||||||
# Requires a personal access token with Actions Read and write permissions on calcom/docker.
|
platform: "linux/amd64"
|
||||||
github-token: "${{ secrets.DOCKER_REPO_ACCESS_TOKEN }}"
|
platform-suffix: ""
|
||||||
script: |
|
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
try {
|
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
const response = await github.rest.actions.createWorkflowDispatch({
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
owner: context.repo.owner,
|
postgres-user: ${{ env.POSTGRES_USER }}
|
||||||
repo: 'docker',
|
postgres-password: ${{ env.POSTGRES_PASSWORD }}
|
||||||
workflow_id: 'create-release.yaml',
|
postgres-db: ${{ env.POSTGRES_DB }}
|
||||||
ref: 'main',
|
database-host: ${{ env.DATABASE_HOST }}
|
||||||
inputs: {
|
push-image: "true"
|
||||||
"RELEASE_TAG": process.env.RELEASE_TAG
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(response);
|
- name: Notify Slack on Success
|
||||||
} catch (error) {
|
if: success()
|
||||||
console.error(error);
|
uses: slackapi/slack-github-action@v1.24.0
|
||||||
core.setFailed(error.message);
|
with:
|
||||||
|
payload: |
|
||||||
|
{
|
||||||
|
"text": ":large_green_circle: Workflow *${{ github.workflow }}* (AMD64) succeeded in job *${{ github.job }}*.\nSee: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||||
}
|
}
|
||||||
|
env:
|
||||||
|
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }}
|
||||||
|
|
||||||
|
- name: Notify Slack on Failure
|
||||||
|
if: failure()
|
||||||
|
uses: slackapi/slack-github-action@v1.24.0
|
||||||
|
with:
|
||||||
|
payload: |
|
||||||
|
{
|
||||||
|
"text": ":red_circle: Workflow *${{ github.workflow }}* (AMD64) failed in job *${{ github.job }}*.\nSee: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||||
|
}
|
||||||
|
env:
|
||||||
|
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }}
|
||||||
|
|
||||||
|
release-arm:
|
||||||
|
name: "Release ARM"
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: "Determine tag"
|
||||||
|
run: 'echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV'
|
||||||
|
|
||||||
|
- name: Build and test Docker image
|
||||||
|
uses: ./.github/actions/docker-build-and-test
|
||||||
|
with:
|
||||||
|
platform: "arm64"
|
||||||
|
platform-suffix: "-arm"
|
||||||
|
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
postgres-user: ${{ env.POSTGRES_USER }}
|
||||||
|
postgres-password: ${{ env.POSTGRES_PASSWORD }}
|
||||||
|
postgres-db: ${{ env.POSTGRES_DB }}
|
||||||
|
database-host: ${{ env.DATABASE_HOST }}
|
||||||
|
push-image: "true"
|
||||||
|
|
||||||
|
- name: Notify Slack on Success
|
||||||
|
if: success()
|
||||||
|
uses: slackapi/slack-github-action@v1.24.0
|
||||||
|
with:
|
||||||
|
payload: |
|
||||||
|
{
|
||||||
|
"text": ":large_green_circle: Workflow *${{ github.workflow }}* (ARM) succeeded in job *${{ github.job }}*.\nSee: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||||
|
}
|
||||||
|
env:
|
||||||
|
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }}
|
||||||
|
|
||||||
|
- name: Notify Slack on Failure
|
||||||
|
if: failure()
|
||||||
|
uses: slackapi/slack-github-action@v1.24.0
|
||||||
|
with:
|
||||||
|
payload: |
|
||||||
|
{
|
||||||
|
"text": ":red_circle: Workflow *${{ github.workflow }}* (ARM) failed in job *${{ github.job }}*.\nSee: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||||
|
}
|
||||||
|
env:
|
||||||
|
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }}
|
||||||
@@ -67,6 +67,9 @@ tsconfig.tsbuildinfo
|
|||||||
|
|
||||||
# Prisma generated files
|
# Prisma generated files
|
||||||
packages/prisma/client/*
|
packages/prisma/client/*
|
||||||
|
!packages/prisma/client/index.ts
|
||||||
|
apps/api/v2/generated/prisma/*
|
||||||
|
packages/prisma/generated/prisma/*
|
||||||
packages/prisma/zod/**/*.ts
|
packages/prisma/zod/**/*.ts
|
||||||
packages/prisma/enums
|
packages/prisma/enums
|
||||||
|
|
||||||
|
|||||||
@@ -16,5 +16,4 @@ public
|
|||||||
packages/prisma/zod
|
packages/prisma/zod
|
||||||
packages/prisma/enums
|
packages/prisma/enums
|
||||||
apps/web/public/embed
|
apps/web/public/embed
|
||||||
apps/api/v2/swagger/documentation.json
|
|
||||||
packages/ui/components/icon/dynamicIconImports.tsx
|
packages/ui/components/icon/dynamicIconImports.tsx
|
||||||
Vendored
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit"
|
"source.fixAll.eslint": "explicit"
|
||||||
},
|
},
|
||||||
|
|||||||
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
FROM --platform=$BUILDPLATFORM node:20 AS builder
|
||||||
|
|
||||||
|
WORKDIR /calcom
|
||||||
|
|
||||||
|
## If we want to read any ENV variable from .env file, we need to first accept and pass it as an argument to the Dockerfile
|
||||||
|
ARG NEXT_PUBLIC_LICENSE_CONSENT
|
||||||
|
ARG NEXT_PUBLIC_WEBSITE_TERMS_URL
|
||||||
|
ARG NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL
|
||||||
|
ARG CALCOM_TELEMETRY_DISABLED
|
||||||
|
ARG DATABASE_URL
|
||||||
|
ARG NEXTAUTH_SECRET=secret
|
||||||
|
ARG CALENDSO_ENCRYPTION_KEY=secret
|
||||||
|
ARG MAX_OLD_SPACE_SIZE=4096
|
||||||
|
ARG NEXT_PUBLIC_API_V2_URL
|
||||||
|
ARG CSP_POLICY
|
||||||
|
|
||||||
|
## We need these variables as required by Next.js build to create rewrites
|
||||||
|
ARG NEXT_PUBLIC_SINGLE_ORG_SLUG
|
||||||
|
ARG ORGANIZATIONS_ENABLED
|
||||||
|
|
||||||
|
ENV NEXT_PUBLIC_WEBAPP_URL=http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER \
|
||||||
|
NEXT_PUBLIC_API_V2_URL=$NEXT_PUBLIC_API_V2_URL \
|
||||||
|
NEXT_PUBLIC_LICENSE_CONSENT=$NEXT_PUBLIC_LICENSE_CONSENT \
|
||||||
|
NEXT_PUBLIC_WEBSITE_TERMS_URL=$NEXT_PUBLIC_WEBSITE_TERMS_URL \
|
||||||
|
NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL=$NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL \
|
||||||
|
CALCOM_TELEMETRY_DISABLED=$CALCOM_TELEMETRY_DISABLED \
|
||||||
|
DATABASE_URL=$DATABASE_URL \
|
||||||
|
DATABASE_DIRECT_URL=$DATABASE_URL \
|
||||||
|
NEXTAUTH_SECRET=${NEXTAUTH_SECRET} \
|
||||||
|
CALENDSO_ENCRYPTION_KEY=${CALENDSO_ENCRYPTION_KEY} \
|
||||||
|
NEXT_PUBLIC_SINGLE_ORG_SLUG=$NEXT_PUBLIC_SINGLE_ORG_SLUG \
|
||||||
|
ORGANIZATIONS_ENABLED=$ORGANIZATIONS_ENABLED \
|
||||||
|
NODE_OPTIONS=--max-old-space-size=${MAX_OLD_SPACE_SIZE} \
|
||||||
|
BUILD_STANDALONE=true \
|
||||||
|
CSP_POLICY=$CSP_POLICY
|
||||||
|
|
||||||
|
COPY package.json yarn.lock .yarnrc.yml playwright.config.ts turbo.json i18n.json ./
|
||||||
|
COPY .yarn ./.yarn
|
||||||
|
COPY apps/web ./apps/web
|
||||||
|
COPY apps/api/v2 ./apps/api/v2
|
||||||
|
COPY packages ./packages
|
||||||
|
COPY tests ./tests
|
||||||
|
|
||||||
|
RUN yarn config set httpTimeout 1200000
|
||||||
|
RUN npx turbo prune --scope=@calcom/web --scope=@calcom/trpc --docker
|
||||||
|
RUN yarn install
|
||||||
|
# Build and make embed servable from web/public/embed folder
|
||||||
|
RUN yarn workspace @calcom/trpc run build
|
||||||
|
RUN yarn --cwd packages/embeds/embed-core workspace @calcom/embed-core run build
|
||||||
|
RUN yarn --cwd apps/web workspace @calcom/web run build
|
||||||
|
RUN rm -rf node_modules/.cache .yarn/cache apps/web/.next/cache
|
||||||
|
|
||||||
|
FROM node:20 AS builder-two
|
||||||
|
|
||||||
|
WORKDIR /calcom
|
||||||
|
ARG NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
COPY package.json .yarnrc.yml turbo.json i18n.json ./
|
||||||
|
COPY .yarn ./.yarn
|
||||||
|
COPY --from=builder /calcom/yarn.lock ./yarn.lock
|
||||||
|
COPY --from=builder /calcom/node_modules ./node_modules
|
||||||
|
COPY --from=builder /calcom/packages ./packages
|
||||||
|
COPY --from=builder /calcom/apps/web ./apps/web
|
||||||
|
COPY --from=builder /calcom/packages/prisma/schema.prisma ./prisma/schema.prisma
|
||||||
|
COPY scripts scripts
|
||||||
|
RUN chmod +x scripts/*
|
||||||
|
|
||||||
|
# Save value used during this build stage. If NEXT_PUBLIC_WEBAPP_URL and BUILT_NEXT_PUBLIC_WEBAPP_URL differ at
|
||||||
|
# run-time, then start.sh will find/replace static values again.
|
||||||
|
ENV NEXT_PUBLIC_WEBAPP_URL=$NEXT_PUBLIC_WEBAPP_URL \
|
||||||
|
BUILT_NEXT_PUBLIC_WEBAPP_URL=$NEXT_PUBLIC_WEBAPP_URL
|
||||||
|
|
||||||
|
RUN scripts/replace-placeholder.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_WEBAPP_URL}
|
||||||
|
|
||||||
|
FROM node:20 AS runner
|
||||||
|
|
||||||
|
WORKDIR /calcom
|
||||||
|
COPY --from=builder-two /calcom ./
|
||||||
|
ARG NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
|
||||||
|
ENV NEXT_PUBLIC_WEBAPP_URL=$NEXT_PUBLIC_WEBAPP_URL \
|
||||||
|
BUILT_NEXT_PUBLIC_WEBAPP_URL=$NEXT_PUBLIC_WEBAPP_URL
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=30s --retries=5 \
|
||||||
|
CMD wget --spider http://localhost:3000 || exit 1
|
||||||
|
|
||||||
|
CMD ["/calcom/scripts/start.sh"]
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<img src="https://user-images.githubusercontent.com/8019099/210054112-5955e812-a76e-4160-9ddd-58f2c72f1cce.png" alt="Logo">
|
<img src="https://user-images.githubusercontent.com/8019099/210054112-5955e812-a76e-4160-9ddd-58f2c72f1cce.png" alt="Logo">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<h3 align="center">Cal.com (formerly Calendso)</h3>
|
<h3 align="center">Cal.com</h3>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
The open-source Calendly successor.
|
The open-source Calendly successor.
|
||||||
@@ -175,6 +175,7 @@ yarn dx
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Development tip
|
#### Development tip
|
||||||
|
|
||||||
1. Add `export NODE_OPTIONS=“--max-old-space-size=16384”` to your shell script to increase the memory limit for the node process. Alternatively, you can run this in your terminal before running the app. Replace 16384 with the amount of RAM you want to allocate to the node process.
|
1. Add `export NODE_OPTIONS=“--max-old-space-size=16384”` to your shell script to increase the memory limit for the node process. Alternatively, you can run this in your terminal before running the app. Replace 16384 with the amount of RAM you want to allocate to the node process.
|
||||||
|
|
||||||
2. Add `NEXT_PUBLIC_LOGGER_LEVEL={level}` to your .env file to control the logging verbosity for all tRPC queries and mutations.\
|
2. Add `NEXT_PUBLIC_LOGGER_LEVEL={level}` to your .env file to control the logging verbosity for all tRPC queries and mutations.\
|
||||||
@@ -234,9 +235,10 @@ for Logger level to be set at info, for example.
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
If you don't want to create a local DB. Then you can also consider using services like railway.app or render.
|
If you don't want to create a local DB. Then you can also consider using services like railway.app, Northflank or render.
|
||||||
|
|
||||||
- [Setup postgres DB with railway.app](https://docs.railway.app/guides/postgresql)
|
- [Setup postgres DB with railway.app](https://docs.railway.app/guides/postgresql)
|
||||||
|
- [Setup postgres DB with Northflank](https://northflank.com/guides/deploy-postgres-database-on-northflank)
|
||||||
- [Setup postgres DB with render](https://render.com/docs/databases)
|
- [Setup postgres DB with render](https://render.com/docs/databases)
|
||||||
|
|
||||||
1. Copy and paste your `DATABASE_URL` from `.env` to `.env.appStore`.
|
1. Copy and paste your `DATABASE_URL` from `.env` to `.env.appStore`.
|
||||||
@@ -374,15 +376,268 @@ Executable doesn't exist at /Users/alice/Library/Caches/ms-playwright/chromium-1
|
|||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
The Docker configuration for Cal.com is an effort powered by people within the community.
|
**Official support**: Our team will begin to officially support the Dockerfile and docker-compose resources in this
|
||||||
|
repository.
|
||||||
|
|
||||||
If you want to contribute to the Docker repository, [reply here](https://github.com/calcom/docker/discussions/32).
|
**Important**: Cal.com will **not** be supporting installations that use these Docker resources. While we provide and maintain the Docker configurations, support for Docker-based installations is the responsibility of the user.
|
||||||
|
|
||||||
The Docker configuration can be found [in our docker repository](https://github.com/calcom/docker).
|
This image can be found on DockerHub at [https://hub.docker.com/r/calcom/cal.com](https://hub.docker.com/r/calcom/cal.com).
|
||||||
|
|
||||||
Issues with Docker? Find your answer or open a new discussion [here](https://github.com/calcom/docker/discussions) to ask the community.
|
**Note for ARM Users**: Use the {version}-arm suffix for pulling images. Example: `docker pull calcom/cal.com:v5.6.19-arm`.
|
||||||
|
|
||||||
Cal.com, Inc. does not provide official support for Docker, but we will accept fixes and documentation. Use at your own risk.
|
#### Requirements
|
||||||
|
|
||||||
|
Make sure you have `docker` & `docker compose` installed on the server / system. Both are installed by most docker utilities, including Docker Desktop and Rancher Desktop.
|
||||||
|
|
||||||
|
Note: `docker compose` without the hyphen is now the primary method of using docker-compose, per the Docker documentation.
|
||||||
|
|
||||||
|
#### (Most users) Running Cal.com with Docker Compose
|
||||||
|
|
||||||
|
If you are evaluating Cal.com or running with minimal to no modifications, this option is for you.
|
||||||
|
|
||||||
|
1. Clone calcom/cal.com
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --recursive https://github.com/calcom/cal.com.git
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Change into the directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd cal.com
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Prepare your configuration: Rename `.env.example` to `.env` and then update `.env`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Most configurations can be left as-is, but for configuration options see [Important Run-time variables](#important-run-time-variables) below.
|
||||||
|
|
||||||
|
**Push Notifications (VAPID Keys)**
|
||||||
|
If you see an error like:
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: No key set vapidDetails.publicKey
|
||||||
|
```
|
||||||
|
|
||||||
|
This means your environment variables for Web Push are missing.
|
||||||
|
You must generate and set `NEXT_PUBLIC_VAPID_PUBLIC_KEY` and `VAPID_PRIVATE_KEY`.
|
||||||
|
|
||||||
|
Generate them with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx web-push generate-vapid-keys
|
||||||
|
```
|
||||||
|
|
||||||
|
Then update your `.env` file:
|
||||||
|
|
||||||
|
```env
|
||||||
|
NEXT_PUBLIC_VAPID_PUBLIC_KEY=your_public_key_here
|
||||||
|
VAPID_PRIVATE_KEY=your_private_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
Do **not** commit real keys to `.env.example` — only placeholders.
|
||||||
|
|
||||||
|
Update the appropriate values in your .env file, then proceed.
|
||||||
|
|
||||||
|
4. (optional) Pre-Pull the images by running the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose pull
|
||||||
|
```
|
||||||
|
|
||||||
|
This will use the default image locations as specified by `image:` in the docker-compose.yaml file.
|
||||||
|
|
||||||
|
Note: To aid with support, by default Scarf.sh is used as registry proxy for download metrics.
|
||||||
|
|
||||||
|
5. Start Cal.com via docker compose
|
||||||
|
|
||||||
|
(Most basic users, and for First Run) To run the complete stack, which includes a local Postgres database, Cal.com web app, and Prisma Studio:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
To run Cal.com web app and Prisma Studio against a remote database, ensure that DATABASE_URL is configured for an available database and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d calcom studio
|
||||||
|
```
|
||||||
|
|
||||||
|
To run only the Cal.com web app, ensure that DATABASE_URL is configured for an available database and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d calcom
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note: to run in attached mode for debugging, remove `-d` from your desired run command.**
|
||||||
|
|
||||||
|
6. Open a browser to [http://localhost:3000](http://localhost:3000), or your defined NEXT_PUBLIC_WEBAPP_URL. The first time you run Cal.com, a setup wizard will initialize. Define your first user, and you're ready to go!
|
||||||
|
|
||||||
|
#### Updating Cal.com
|
||||||
|
|
||||||
|
1. Stop the Cal.com stack
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Pull the latest changes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose pull
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Update env vars as necessary.
|
||||||
|
4. Re-start the Cal.com stack
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
#### (Advanced users) Build and Run Cal.com
|
||||||
|
|
||||||
|
1. Clone calcom/docker.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/calcom/cal.com.git calcom-docker
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Change into the directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd calcom-docker
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Update the calcom submodule. This project depends on the Cal.com source code, which is included here as a Git submodule. To make sure you get everything you need, update the submodule with the command below.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git submodule update --remote --init
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: DO NOT use recursive submodule update, otherwise you will receive a git authentication error.
|
||||||
|
|
||||||
|
4. Rename `.env.example` to `.env` and then update `.env`
|
||||||
|
|
||||||
|
For configuration options see [Build-time variables](#build-time-variables) below. Update the appropriate values in your .env file, then proceed.
|
||||||
|
|
||||||
|
5. Build the Cal.com docker image:
|
||||||
|
|
||||||
|
Note: Due to application configuration requirements, an available database is currently required during the build process.
|
||||||
|
|
||||||
|
a) If hosting elsewhere, configure the `DATABASE_URL` in the .env file, and skip the next step
|
||||||
|
|
||||||
|
b) If a local or temporary database is required, start a local database via docker compose.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d database
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Build Cal.com via docker compose (DOCKER_BUILDKIT=0 must be provided to allow a network bridge to be used at build time. This requirement will be removed in the future)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DOCKER_BUILDKIT=0 docker compose build calcom
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Start Cal.com via docker compose
|
||||||
|
|
||||||
|
(Most basic users, and for First Run) To run the complete stack, which includes a local Postgres database, Cal.com web app, and Prisma Studio:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
To run Cal.com web app and Prisma Studio against a remote database, ensure that DATABASE_URL is configured for an available database and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d calcom studio
|
||||||
|
```
|
||||||
|
|
||||||
|
To run only the Cal.com web app, ensure that DATABASE_URL is configured for an available database and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d calcom
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note: to run in attached mode for debugging, remove `-d` from your desired run command.**
|
||||||
|
|
||||||
|
8. Open a browser to [http://localhost:3000](http://localhost:3000), or your defined NEXT_PUBLIC_WEBAPP_URL. The first time you run Cal.com, a setup wizard will initialize. Define your first user, and you're ready to go!
|
||||||
|
|
||||||
|
#### Configuration
|
||||||
|
|
||||||
|
##### Important Run-time variables
|
||||||
|
|
||||||
|
These variables must also be provided at runtime
|
||||||
|
|
||||||
|
| Variable | Description | Required | Default |
|
||||||
|
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------- |
|
||||||
|
| DATABASE_URL | database url with credentials - if using a connection pooler, this setting should point there | required | `postgresql://unicorn_user:magical_password@database:5432/calendso` |
|
||||||
|
| CALCOM_LICENSE_KEY | Enterprise License Key | optional | |
|
||||||
|
| NEXT_PUBLIC_WEBAPP_URL | Base URL of the site. NOTE: if this value differs from the value used at build-time, there will be a slight delay during container start (to update the statically built files). | optional | `http://localhost:3000` |
|
||||||
|
| NEXTAUTH_URL | Location of the auth server. By default, this is the Cal.com docker instance itself. | optional | `{NEXT_PUBLIC_WEBAPP_URL}/api/auth` |
|
||||||
|
| NEXTAUTH_SECRET | must match build variable | required | `secret` |
|
||||||
|
| CALENDSO_ENCRYPTION_KEY | must match build variable | required | `secret` |
|
||||||
|
|
||||||
|
##### Build-time variables
|
||||||
|
|
||||||
|
If building the image yourself, these variables must be provided at the time of the docker build, and can be provided by updating the .env file. Currently, if you require changes to these variables, you must follow the instructions to build and publish your own image.
|
||||||
|
|
||||||
|
Updating these variables is not required for evaluation, but is required for running in production. Instructions for generating variables can be found in the [Cal.com instructions](https://github.com/calcom/cal.com)
|
||||||
|
|
||||||
|
| Variable | Description | Required | Default |
|
||||||
|
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------- |
|
||||||
|
| DATABASE_URL | database url with credentials - if using a connection pooler, this setting should point there | required | `postgresql://unicorn_user:magical_password@database:5432/calendso` |
|
||||||
|
| MAX_OLD_SPACE_SIZE | Needed for Nodejs/NPM build options | required | 4096 |
|
||||||
|
| NEXT_PUBLIC_LICENSE_CONSENT | license consent - true/false | required | |
|
||||||
|
| NEXTAUTH_SECRET | Cookie encryption key | required | `secret` |
|
||||||
|
| CALENDSO_ENCRYPTION_KEY | Authentication encryption key | required | `secret` |
|
||||||
|
| NEXT_PUBLIC_WEBAPP_URL | Base URL injected into static files | optional | `http://localhost:3000` |
|
||||||
|
| NEXT_PUBLIC_WEBSITE_TERMS_URL | custom URL for terms and conditions website | optional | `https://cal.com/terms` |
|
||||||
|
| NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL | custom URL for privacy policy website | optional | `https://cal.com/privacy` |
|
||||||
|
| NEXT_PUBLIC_API_V2_URL | URL for the v2 API, only required for custom integrations or custom booking experiences using [Cal.com Platform](https://cal.com/platform) | optional | |
|
||||||
|
| CALCOM_TELEMETRY_DISABLED | Allow Cal.com to collect anonymous usage data (set to `1` to disable) | optional | |
|
||||||
|
| NEXT_PUBLIC_SINGLE_ORG_SLUG | Required if ORGANIZATIONS_ENABLED is true | optional | |
|
||||||
|
| ORGANIZATIONS_ENABLED | Used for Enterprise or Organizations plan | optional | |
|
||||||
|
|
||||||
|
#### Troubleshooting
|
||||||
|
|
||||||
|
##### SSL edge termination
|
||||||
|
|
||||||
|
If running behind a load balancer which handles SSL certificates, you will need to add the environmental variable `NODE_TLS_REJECT_UNAUTHORIZED=0` to prevent requests from being rejected. Only do this if you know what you are doing and trust the services/load-balancers directing traffic to your service.
|
||||||
|
|
||||||
|
##### Failed to commit changes: Invalid 'prisma.user.create()'
|
||||||
|
|
||||||
|
Certain versions may have trouble creating a user if the field `metadata` is empty. Using an empty json object `{}` as the field value should resolve this issue. Also, the `id` field will autoincrement, so you may also try leaving the value of `id` as empty.
|
||||||
|
|
||||||
|
##### CLIENT_FETCH_ERROR
|
||||||
|
|
||||||
|
If you experience this error, it may be the way the default Auth callback in the server is using the WEBAPP_URL as a base url. The container does not necessarily have access to the same DNS as your local machine, and therefor needs to be configured to resolve to itself. You may be able to correct this by configuring `NEXTAUTH_URL=http://localhost:3000/api/auth`, to help the backend loop back to itself.
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-calcom-1 | @calcom/web:start: [next-auth][error][CLIENT_FETCH_ERROR]
|
||||||
|
docker-calcom-1 | @calcom/web:start: https://next-auth.js.org/errors#client_fetch_error request to http://testing.localhost:3000/api/auth/session failed, reason: getaddrinfo ENOTFOUND testing.localhost {
|
||||||
|
docker-calcom-1 | @calcom/web:start: error: {
|
||||||
|
docker-calcom-1 | @calcom/web:start: message: 'request to http://testing.localhost:3000/api/auth/session failed, reason: getaddrinfo ENOTFOUND testing.localhost',
|
||||||
|
docker-calcom-1 | @calcom/web:start: stack: 'FetchError: request to http://testing.localhost:3000/api/auth/session failed, reason: getaddrinfo ENOTFOUND testing.localhost\n' +
|
||||||
|
docker-calcom-1 | @calcom/web:start: ' at ClientRequest.<anonymous> (/calcom/node_modules/next/dist/compiled/node-fetch/index.js:1:65756)\n' +
|
||||||
|
docker-calcom-1 | @calcom/web:start: ' at ClientRequest.emit (node:events:513:28)\n' +
|
||||||
|
docker-calcom-1 | @calcom/web:start: ' at ClientRequest.emit (node:domain:489:12)\n' +
|
||||||
|
docker-calcom-1 | @calcom/web:start: ' at Socket.socketErrorListener (node:_http_client:494:9)\n' +
|
||||||
|
docker-calcom-1 | @calcom/web:start: ' at Socket.emit (node:events:513:28)\n' +
|
||||||
|
docker-calcom-1 | @calcom/web:start: ' at Socket.emit (node:domain:489:12)\n' +
|
||||||
|
docker-calcom-1 | @calcom/web:start: ' at emitErrorNT (node:internal/streams/destroy:157:8)\n' +
|
||||||
|
docker-calcom-1 | @calcom/web:start: ' at emitErrorCloseNT (node:internal/streams/destroy:122:3)\n' +
|
||||||
|
docker-calcom-1 | @calcom/web:start: ' at processTicksAndRejections (node:internal/process/task_queues:83:21)',
|
||||||
|
docker-calcom-1 | @calcom/web:start: name: 'FetchError'
|
||||||
|
docker-calcom-1 | @calcom/web:start: },
|
||||||
|
docker-calcom-1 | @calcom/web:start: url: 'http://testing.localhost:3000/api/auth/session',
|
||||||
|
docker-calcom-1 | @calcom/web:start: message: 'request to http://testing.localhost:3000/api/auth/session failed, reason: getaddrinfo ENOTFOUND testing.localhost'
|
||||||
|
docker-calcom-1 | @calcom/web:start: }
|
||||||
|
```
|
||||||
|
|
||||||
|
<img referrerpolicy="no-referrer-when-downgrade" src="https://static.scarf.sh/a.png?x-pxid=81cda9f7-a102-453b-ac01-51c35650bd70" />
|
||||||
|
|
||||||
### Railway
|
### Railway
|
||||||
|
|
||||||
@@ -390,6 +645,12 @@ Cal.com, Inc. does not provide official support for Docker, but we will accept f
|
|||||||
|
|
||||||
You can deploy Cal.com on [Railway](https://railway.app) using the button above. The team at Railway also have a [detailed blog post](https://blog.railway.app/p/calendso) on deploying Cal.com on their platform.
|
You can deploy Cal.com on [Railway](https://railway.app) using the button above. The team at Railway also have a [detailed blog post](https://blog.railway.app/p/calendso) on deploying Cal.com on their platform.
|
||||||
|
|
||||||
|
### Northflank
|
||||||
|
|
||||||
|
[](https://northflank.com/stacks/deploy-calcom)
|
||||||
|
|
||||||
|
You can deploy Cal.com on [Northflank](https://northflank.com) using the button above. The team at Northflank also have a [detailed blog post](https://northflank.com/guides/deploy-calcom-with-northflank) on deploying Cal.com on their platform.
|
||||||
|
|
||||||
### Vercel
|
### Vercel
|
||||||
|
|
||||||
Currently Vercel Pro Plan is required to be able to Deploy this application with Vercel, due to limitations on the number of serverless functions on the free plan.
|
Currently Vercel Pro Plan is required to be able to Deploy this application with Vercel, due to limitations on the number of serverless functions on the free plan.
|
||||||
@@ -490,8 +751,8 @@ Don't code but still want to contribute? Join our [Discussions](https://github.c
|
|||||||
- Set CSP_POLICY="non-strict" env variable, which enables [Strict CSP](https://web.dev/strict-csp/) except for unsafe-inline in style-src . If you have some custom changes in your instance, you might have to make some code change to make your instance CSP compatible. Right now it enables strict CSP only on login page and on other SSR pages it is enabled in Report only mode to detect possible issues. On, SSG pages it is still not supported.
|
- Set CSP_POLICY="non-strict" env variable, which enables [Strict CSP](https://web.dev/strict-csp/) except for unsafe-inline in style-src . If you have some custom changes in your instance, you might have to make some code change to make your instance CSP compatible. Right now it enables strict CSP only on login page and on other SSR pages it is enabled in Report only mode to detect possible issues. On, SSG pages it is still not supported.
|
||||||
|
|
||||||
## Single Org Mode
|
## Single Org Mode
|
||||||
Refer to docs [here](./docs/self-hosting/guides/organization/single-organization-setup) for a detailed documentation with screenshots.
|
|
||||||
|
|
||||||
|
Refer to docs [here](./docs/self-hosting/guides/organization/single-organization-setup) for a detailed documentation with screenshots.
|
||||||
|
|
||||||
## Integrations
|
## Integrations
|
||||||
|
|
||||||
@@ -648,6 +909,7 @@ following
|
|||||||
We use changesets to generate changelogs and publish public packages (packages with `private: true` are ignored).
|
We use changesets to generate changelogs and publish public packages (packages with `private: true` are ignored).
|
||||||
|
|
||||||
An example of good readme is [atoms readme](https://github.com/calcom/cal.com/blob/main/packages/platform/atoms/README.md). Every public package must:
|
An example of good readme is [atoms readme](https://github.com/calcom/cal.com/blob/main/packages/platform/atoms/README.md). Every public package must:
|
||||||
|
|
||||||
1. Follow semantic versioning when using changesets.
|
1. Follow semantic versioning when using changesets.
|
||||||
2. Mark breaking changes using `❗️Breaking change`
|
2. Mark breaking changes using `❗️Breaking change`
|
||||||
|
|
||||||
|
|||||||
@@ -13,5 +13,8 @@
|
|||||||
"connect": "^3.7.0",
|
"connect": "^3.7.0",
|
||||||
"http": "^0.0.1-security",
|
"http": "^0.0.1-security",
|
||||||
"http-proxy-middleware": "^2.0.6"
|
"http-proxy-middleware": "^2.0.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@calcom/eslint-config": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { nextJsConfig } from "@calcom/eslint-config/next-js";
|
||||||
|
|
||||||
|
export default nextJsConfig;
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { get } from "@vercel/edge-config";
|
|
||||||
import type { NextMiddleware } from "next-api-middleware";
|
|
||||||
|
|
||||||
const safeGet = async <T = unknown>(key: string): Promise<T | undefined> => {
|
|
||||||
try {
|
|
||||||
return get<T>(key);
|
|
||||||
} catch (error) {
|
|
||||||
// Don't crash if EDGE_CONFIG env var is missing
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const config = { matcher: "/:path*" };
|
|
||||||
|
|
||||||
export const checkIsInMaintenanceMode: NextMiddleware = async (req, res, next) => {
|
|
||||||
const isInMaintenanceMode = await safeGet<boolean>("isInMaintenanceMode");
|
|
||||||
if (isInMaintenanceMode) {
|
|
||||||
return res
|
|
||||||
.status(503)
|
|
||||||
.json({ message: "API is currently under maintenance. Please try again at a later time." });
|
|
||||||
}
|
|
||||||
|
|
||||||
await next();
|
|
||||||
};
|
|
||||||
@@ -4,7 +4,7 @@ import type { NextApiResponse, NextApiRequest } from "next";
|
|||||||
import { createMocks } from "node-mocks-http";
|
import { createMocks } from "node-mocks-http";
|
||||||
import { describe, it, expect, vi } from "vitest";
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
|
||||||
import { handleAutoLock } from "@calcom/lib/autoLock";
|
import { handleAutoLock } from "@calcom/features/ee/api-keys/lib/autoLock";
|
||||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ vi.mock("@calcom/lib/checkRateLimitAndThrowError", () => ({
|
|||||||
checkRateLimitAndThrowError: vi.fn(),
|
checkRateLimitAndThrowError: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@calcom/lib/autoLock", () => ({
|
vi.mock("@calcom/features/ee/api-keys/lib/autoLock", () => ({
|
||||||
handleAutoLock: vi.fn(),
|
handleAutoLock: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { NextMiddleware } from "next-api-middleware";
|
import type { NextMiddleware } from "next-api-middleware";
|
||||||
|
|
||||||
import { handleAutoLock } from "@calcom/lib/autoLock";
|
import { handleAutoLock } from "@calcom/features/ee/api-keys/lib/autoLock";
|
||||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,42 @@
|
|||||||
import prismock from "../../../../../tests/libs/__mocks__/prisma";
|
/**
|
||||||
|
* Unit Tests for verifyApiKey middleware
|
||||||
|
*
|
||||||
|
* These tests verify the middleware logic without touching the database.
|
||||||
|
* All dependencies (repositories, utilities) are mocked.
|
||||||
|
*/
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { createMocks } from "node-mocks-http";
|
import { createMocks } from "node-mocks-http";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import type { ILicenseKeyService } from "@calcom/ee/common/server/LicenseKeyService";
|
import type { ILicenseKeyService } from "@calcom/ee/common/server/LicenseKeyService";
|
||||||
import LicenseKeyService from "@calcom/ee/common/server/LicenseKeyService";
|
import LicenseKeyService, { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService";
|
||||||
import { hashAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
|
import { PrismaApiKeyRepository } from "@calcom/lib/server/repository/PrismaApiKeyRepository";
|
||||||
import type { IDeploymentRepository } from "@calcom/lib/server/repository/deployment.interface";
|
import type { IDeploymentRepository } from "@calcom/lib/server/repository/deployment.interface";
|
||||||
import prisma from "@calcom/prisma";
|
import { ApiKeyService } from "@calcom/lib/server/service/ApiKeyService";
|
||||||
import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
|
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
|
import { isAdminGuard } from "../utils/isAdmin";
|
||||||
|
import { isLockedOrBlocked } from "../utils/isLockedOrBlocked";
|
||||||
|
import { ScopeOfAdmin } from "../utils/scopeOfAdmin";
|
||||||
import { verifyApiKey } from "./verifyApiKey";
|
import { verifyApiKey } from "./verifyApiKey";
|
||||||
|
|
||||||
|
vi.mock("@calcom/lib/server/service/ApiKeyService", () => ({
|
||||||
|
ApiKeyService: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@calcom/lib/server/repository/PrismaApiKeyRepository", () => ({
|
||||||
|
PrismaApiKeyRepository: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../utils/isAdmin", () => ({
|
||||||
|
isAdminGuard: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../utils/isLockedOrBlocked", () => ({
|
||||||
|
isLockedOrBlocked: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("@calcom/lib/crypto", () => ({
|
vi.mock("@calcom/lib/crypto", () => ({
|
||||||
symmetricDecrypt: vi.fn().mockReturnValue("mocked-decrypted-value"),
|
symmetricDecrypt: vi.fn().mockReturnValue("mocked-decrypted-value"),
|
||||||
symmetricEncrypt: vi.fn().mockReturnValue("mocked-encrypted-value"),
|
symmetricEncrypt: vi.fn().mockReturnValue("mocked-encrypted-value"),
|
||||||
@@ -32,17 +55,29 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mockDeploymentRepository: IDeploymentRepository = {
|
const mockDeploymentRepository: IDeploymentRepository = {
|
||||||
getLicenseKeyWithId: vi.fn().mockResolvedValue("mockLicenseKey"), // Mocked return value
|
getLicenseKeyWithId: vi.fn().mockResolvedValue("mockLicenseKey"),
|
||||||
getSignatureToken: vi.fn().mockResolvedValue("mockSignatureToken"),
|
getSignatureToken: vi.fn().mockResolvedValue("mockSignatureToken"),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("Verify API key", () => {
|
describe("Verify API key - Unit Tests", () => {
|
||||||
let service: ILicenseKeyService;
|
let service: ILicenseKeyService;
|
||||||
|
let mockApiKeyService: ApiKeyService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
service = await LicenseKeyService.create(mockDeploymentRepository);
|
service = await LicenseKeyService.create(mockDeploymentRepository);
|
||||||
|
|
||||||
vi.spyOn(service, "checkLicense");
|
vi.spyOn(service, "checkLicense");
|
||||||
|
|
||||||
|
vi.spyOn(LicenseKeySingleton, "getInstance").mockResolvedValue(service as LicenseKeyService);
|
||||||
|
|
||||||
|
mockApiKeyService = {
|
||||||
|
verifyKeyByHashedKey: vi.fn(),
|
||||||
|
} as unknown as ApiKeyService;
|
||||||
|
|
||||||
|
vi.mocked(ApiKeyService).mockImplementation(() => mockApiKeyService);
|
||||||
|
vi.mocked(PrismaApiKeyRepository).mockImplementation(() => ({} as unknown as PrismaApiKeyRepository));
|
||||||
|
|
||||||
|
vi.mocked(isAdminGuard).mockReset();
|
||||||
|
vi.mocked(isLockedOrBlocked).mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw an error if the api key is not valid", async () => {
|
it("should throw an error if the api key is not valid", async () => {
|
||||||
@@ -98,22 +133,25 @@ describe("Verify API key", () => {
|
|||||||
query: {
|
query: {
|
||||||
apiKey: "cal_test_key",
|
apiKey: "cal_test_key",
|
||||||
},
|
},
|
||||||
prisma,
|
|
||||||
});
|
});
|
||||||
const hashedKey = hashAPIKey("test_key");
|
|
||||||
await prismock.apiKey.create({
|
vi.mocked(mockApiKeyService.verifyKeyByHashedKey).mockResolvedValue({
|
||||||
data: {
|
valid: true,
|
||||||
hashedKey,
|
userId: 1,
|
||||||
user: {
|
user: {
|
||||||
create: {
|
role: UserPermissionRole.ADMIN,
|
||||||
email: "admin@example.com",
|
locked: false,
|
||||||
role: UserPermissionRole.ADMIN,
|
email: "admin@example.com",
|
||||||
locked: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mocked(isAdminGuard).mockResolvedValue({
|
||||||
|
isAdmin: true,
|
||||||
|
scope: ScopeOfAdmin.SystemWide,
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mocked(isLockedOrBlocked).mockResolvedValue(false);
|
||||||
|
|
||||||
const middleware = {
|
const middleware = {
|
||||||
fn: verifyApiKey,
|
fn: verifyApiKey,
|
||||||
};
|
};
|
||||||
@@ -139,40 +177,25 @@ describe("Verify API key", () => {
|
|||||||
query: {
|
query: {
|
||||||
apiKey: "cal_test_key",
|
apiKey: "cal_test_key",
|
||||||
},
|
},
|
||||||
prisma,
|
|
||||||
});
|
});
|
||||||
const hashedKey = hashAPIKey("test_key");
|
|
||||||
await prismock.apiKey.create({
|
vi.mocked(mockApiKeyService.verifyKeyByHashedKey).mockResolvedValue({
|
||||||
data: {
|
valid: true,
|
||||||
hashedKey,
|
userId: 2,
|
||||||
user: {
|
user: {
|
||||||
create: {
|
role: UserPermissionRole.USER,
|
||||||
email: "org-admin@acme.com",
|
locked: false,
|
||||||
role: UserPermissionRole.USER,
|
email: "org-admin@acme.com",
|
||||||
locked: false,
|
|
||||||
teams: {
|
|
||||||
create: {
|
|
||||||
accepted: true,
|
|
||||||
role: MembershipRole.OWNER,
|
|
||||||
team: {
|
|
||||||
create: {
|
|
||||||
name: "ACME",
|
|
||||||
isOrganization: true,
|
|
||||||
organizationSettings: {
|
|
||||||
create: {
|
|
||||||
isAdminAPIEnabled: true,
|
|
||||||
orgAutoAcceptEmail: "acme.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mocked(isAdminGuard).mockResolvedValue({
|
||||||
|
isAdmin: true,
|
||||||
|
scope: ScopeOfAdmin.OrgOwnerOrAdmin,
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mocked(isLockedOrBlocked).mockResolvedValue(false);
|
||||||
|
|
||||||
const middleware = {
|
const middleware = {
|
||||||
fn: verifyApiKey,
|
fn: verifyApiKey,
|
||||||
};
|
};
|
||||||
@@ -198,22 +221,25 @@ describe("Verify API key", () => {
|
|||||||
query: {
|
query: {
|
||||||
apiKey: "cal_test_key",
|
apiKey: "cal_test_key",
|
||||||
},
|
},
|
||||||
prisma,
|
|
||||||
});
|
});
|
||||||
const hashedKey = hashAPIKey("test_key");
|
|
||||||
await prismock.apiKey.create({
|
vi.mocked(mockApiKeyService.verifyKeyByHashedKey).mockResolvedValue({
|
||||||
data: {
|
valid: true,
|
||||||
hashedKey,
|
userId: 3,
|
||||||
user: {
|
user: {
|
||||||
create: {
|
role: UserPermissionRole.USER,
|
||||||
email: "locked@example.com",
|
locked: true,
|
||||||
role: UserPermissionRole.USER,
|
email: "locked@example.com",
|
||||||
locked: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mocked(isAdminGuard).mockResolvedValue({
|
||||||
|
isAdmin: false,
|
||||||
|
scope: ScopeOfAdmin.SystemWide,
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mocked(isLockedOrBlocked).mockResolvedValue(true);
|
||||||
|
|
||||||
const middleware = {
|
const middleware = {
|
||||||
fn: verifyApiKey,
|
fn: verifyApiKey,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,21 +3,15 @@ import type { NextMiddleware } from "next-api-middleware";
|
|||||||
import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService";
|
import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService";
|
||||||
import { hashAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
|
import { hashAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
|
||||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||||
|
import { PrismaApiKeyRepository } from "@calcom/lib/server/repository/PrismaApiKeyRepository";
|
||||||
import { DeploymentRepository } from "@calcom/lib/server/repository/deployment";
|
import { DeploymentRepository } from "@calcom/lib/server/repository/deployment";
|
||||||
import prisma from "@calcom/prisma";
|
import { ApiKeyService } from "@calcom/lib/server/service/ApiKeyService";
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
|
||||||
import { isAdminGuard } from "../utils/isAdmin";
|
import { isAdminGuard } from "../utils/isAdmin";
|
||||||
import { isLockedOrBlocked } from "../utils/isLockedOrBlocked";
|
import { isLockedOrBlocked } from "../utils/isLockedOrBlocked";
|
||||||
import { ScopeOfAdmin } from "../utils/scopeOfAdmin";
|
import { ScopeOfAdmin } from "../utils/scopeOfAdmin";
|
||||||
|
|
||||||
// Used to check if the apiKey is not expired, could be extracted if reused. but not for now.
|
|
||||||
export const dateNotInPast = function (date: Date) {
|
|
||||||
const now = new Date();
|
|
||||||
if (now.setHours(0, 0, 0, 0) > date.setHours(0, 0, 0, 0)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// This verifies the apiKey and sets the user if it is valid.
|
// This verifies the apiKey and sets the user if it is valid.
|
||||||
export const verifyApiKey: NextMiddleware = async (req, res, next) => {
|
export const verifyApiKey: NextMiddleware = async (req, res, next) => {
|
||||||
const deploymentRepo = new DeploymentRepository(prisma);
|
const deploymentRepo = new DeploymentRepository(prisma);
|
||||||
@@ -32,24 +26,19 @@ export const verifyApiKey: NextMiddleware = async (req, res, next) => {
|
|||||||
|
|
||||||
const strippedApiKey = `${req.query.apiKey}`.replace(process.env.API_KEY_PREFIX || "cal_", "");
|
const strippedApiKey = `${req.query.apiKey}`.replace(process.env.API_KEY_PREFIX || "cal_", "");
|
||||||
const hashedKey = hashAPIKey(strippedApiKey);
|
const hashedKey = hashAPIKey(strippedApiKey);
|
||||||
const apiKey = await prisma.apiKey.findUnique({
|
|
||||||
where: { hashedKey },
|
// Use service layer for API key verification
|
||||||
include: {
|
const apiKeyRepo = new PrismaApiKeyRepository(prisma);
|
||||||
user: {
|
const apiKeyService = new ApiKeyService({ apiKeyRepo });
|
||||||
select: { role: true, locked: true, email: true },
|
const result = await apiKeyService.verifyKeyByHashedKey(hashedKey);
|
||||||
},
|
|
||||||
},
|
if (!result.valid) {
|
||||||
});
|
return res.status(401).json({ error: result.error });
|
||||||
if (!apiKey) return res.status(401).json({ error: "Your API key is not valid." });
|
|
||||||
if (apiKey.expiresAt && dateNotInPast(apiKey.expiresAt)) {
|
|
||||||
return res.status(401).json({ error: "This API key is expired." });
|
|
||||||
}
|
}
|
||||||
if (!apiKey.userId || !apiKey.user)
|
|
||||||
return res.status(404).json({ error: "No user found for this API key." });
|
|
||||||
|
|
||||||
// save the user id in the request for later use
|
// save the user id in the request for later use
|
||||||
req.userId = apiKey.userId;
|
req.userId = result.userId!;
|
||||||
req.user = apiKey.user;
|
req.user = result.user!;
|
||||||
|
|
||||||
const { isAdmin, scope } = await isAdminGuard(req);
|
const { isAdmin, scope } = await isAdminGuard(req);
|
||||||
const userIsLockedOrBlocked = await isLockedOrBlocked(req);
|
const userIsLockedOrBlocked = await isLockedOrBlocked(req);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { label } from "next-api-middleware";
|
|||||||
import { addRequestId } from "./addRequestid";
|
import { addRequestId } from "./addRequestid";
|
||||||
import { captureErrors } from "./captureErrors";
|
import { captureErrors } from "./captureErrors";
|
||||||
import { captureUserId } from "./captureUserId";
|
import { captureUserId } from "./captureUserId";
|
||||||
import { checkIsInMaintenanceMode } from "./checkIsInMaintenanceMode";
|
|
||||||
import { extendRequest } from "./extendRequest";
|
import { extendRequest } from "./extendRequest";
|
||||||
import {
|
import {
|
||||||
HTTP_DELETE,
|
HTTP_DELETE,
|
||||||
@@ -26,7 +25,6 @@ const middleware = {
|
|||||||
HTTP_POST,
|
HTTP_POST,
|
||||||
HTTP_DELETE,
|
HTTP_DELETE,
|
||||||
addRequestId,
|
addRequestId,
|
||||||
checkIsInMaintenanceMode,
|
|
||||||
verifyApiKey,
|
verifyApiKey,
|
||||||
rateLimitApiKey,
|
rateLimitApiKey,
|
||||||
extendRequest,
|
extendRequest,
|
||||||
@@ -38,16 +36,14 @@ const middleware = {
|
|||||||
|
|
||||||
type Middleware = keyof typeof middleware;
|
type Middleware = keyof typeof middleware;
|
||||||
|
|
||||||
const middlewareOrder = [
|
const middlewareOrder: Middleware[] = [
|
||||||
// The order here, determines the order of execution
|
|
||||||
"checkIsInMaintenanceMode",
|
|
||||||
"extendRequest",
|
"extendRequest",
|
||||||
"captureErrors",
|
"captureErrors",
|
||||||
"verifyApiKey",
|
"verifyApiKey",
|
||||||
"rateLimitApiKey",
|
"rateLimitApiKey",
|
||||||
"addRequestId",
|
"addRequestId",
|
||||||
"captureUserId",
|
"captureUserId",
|
||||||
] as Middleware[]; // <-- Provide a list of middleware to call automatically
|
];
|
||||||
|
|
||||||
const withMiddleware = label(middleware, middlewareOrder);
|
const withMiddleware = label(middleware, middlewareOrder);
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const eventTypeSelect = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
owner: {
|
owner: {
|
||||||
select: { username: true, id: true },
|
select: { username: true, id: true, timeZone: true },
|
||||||
},
|
},
|
||||||
children: {
|
children: {
|
||||||
select: {
|
select: {
|
||||||
|
|||||||
@@ -16,13 +16,15 @@ export const isAdminGuard = async (req: NextApiRequest) => {
|
|||||||
where: {
|
where: {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
accepted: true,
|
accepted: true,
|
||||||
|
role: {
|
||||||
|
in: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||||
|
},
|
||||||
team: {
|
team: {
|
||||||
isOrganization: true,
|
isOrganization: true,
|
||||||
organizationSettings: {
|
organizationSettings: {
|
||||||
isAdminAPIEnabled: true,
|
isAdminAPIEnabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
|
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
team: {
|
team: {
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
|
import { sentrySpan } from "@calcom/features/watchlist/lib/telemetry";
|
||||||
import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
|
import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
|
||||||
|
|
||||||
export async function isLockedOrBlocked(req: NextApiRequest) {
|
export async function isLockedOrBlocked(req: NextApiRequest) {
|
||||||
const user = req.user;
|
const user = req.user;
|
||||||
if (!user?.email) return false;
|
if (!user?.email) return false;
|
||||||
return user.locked || (await checkIfEmailIsBlockedInWatchlistController(user.email));
|
return (
|
||||||
|
user.locked ||
|
||||||
|
(await checkIfEmailIsBlockedInWatchlistController({
|
||||||
|
email: user.email,
|
||||||
|
organizationId: null,
|
||||||
|
span: sentrySpan,
|
||||||
|
}))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ const getAllAdminMemberships = async (userId: number) => {
|
|||||||
where: {
|
where: {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
accepted: true,
|
accepted: true,
|
||||||
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
|
role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] },
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
team: {
|
team: {
|
||||||
@@ -56,16 +56,19 @@ export const getAccessibleUsers = async ({
|
|||||||
memberUserIds,
|
memberUserIds,
|
||||||
adminUserId,
|
adminUserId,
|
||||||
}: AccessibleUsersType): Promise<number[]> => {
|
}: AccessibleUsersType): Promise<number[]> => {
|
||||||
|
const orConditions = [];
|
||||||
|
if (memberUserIds.length > 0) {
|
||||||
|
orConditions.push({ userId: { in: memberUserIds } });
|
||||||
|
}
|
||||||
|
orConditions.push({ userId: adminUserId, role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] } });
|
||||||
|
|
||||||
const memberships = await prisma.membership.findMany({
|
const memberships = await prisma.membership.findMany({
|
||||||
where: {
|
where: {
|
||||||
team: {
|
team: {
|
||||||
isOrganization: true,
|
isOrganization: true,
|
||||||
},
|
},
|
||||||
accepted: true,
|
accepted: true,
|
||||||
OR: [
|
OR: orConditions,
|
||||||
{ userId: { in: memberUserIds } },
|
|
||||||
{ userId: adminUserId, role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] } },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
userId: true,
|
userId: true,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { checkUsername } from "@calcom/features/profile/lib/checkUsername";
|
||||||
import { emailSchema } from "@calcom/lib/emailSchema";
|
import { emailSchema } from "@calcom/lib/emailSchema";
|
||||||
import { checkUsername } from "@calcom/lib/server/checkUsername";
|
|
||||||
import { iso8601 } from "@calcom/prisma/zod-utils";
|
import { iso8601 } from "@calcom/prisma/zod-utils";
|
||||||
import { UserSchema } from "@calcom/prisma/zod/modelSchema/UserSchema";
|
import { UserSchema } from "@calcom/prisma/zod/modelSchema/UserSchema";
|
||||||
|
|
||||||
|
|||||||
+11
-10
@@ -10,7 +10,7 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
|
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
|
||||||
"dev": "PORT=3003 next dev",
|
"dev": "PORT=3003 next dev",
|
||||||
"lint": "eslint . --ignore-path .gitignore",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||||
"start": "PORT=3003 next start",
|
"start": "PORT=3003 next start",
|
||||||
"docker-start-api": "PORT=80 next start",
|
"docker-start-api": "PORT=80 next start",
|
||||||
@@ -18,18 +18,19 @@
|
|||||||
"type-check:ci": "tsc-absolute --pretty --noEmit"
|
"type-check:ci": "tsc-absolute --pretty --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/tsconfig": "*",
|
"@calcom/eslint-config": "workspace:*",
|
||||||
"@calcom/types": "*",
|
"@calcom/tsconfig": "workspace:*",
|
||||||
|
"@calcom/types": "workspace:*",
|
||||||
"node-mocks-http": "^1.11.0"
|
"node-mocks-http": "^1.11.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/app-store": "*",
|
"@calcom/app-store": "workspace:*",
|
||||||
"@calcom/dayjs": "*",
|
"@calcom/dayjs": "workspace:*",
|
||||||
"@calcom/emails": "*",
|
"@calcom/emails": "workspace:*",
|
||||||
"@calcom/features": "*",
|
"@calcom/features": "workspace:*",
|
||||||
"@calcom/lib": "*",
|
"@calcom/lib": "workspace:*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "workspace:*",
|
||||||
"@calcom/trpc": "*",
|
"@calcom/trpc": "workspace:*",
|
||||||
"@prisma/nextjs-monorepo-workaround-plugin": "^6.16.1",
|
"@prisma/nextjs-monorepo-workaround-plugin": "^6.16.1",
|
||||||
"@sentry/nextjs": "^9.15.0",
|
"@sentry/nextjs": "^9.15.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getUserAvailabilityService } from "@calcom/lib/di/containers/GetUserAvailability";
|
import { getUserAvailabilityService } from "@calcom/features/di/containers/GetUserAvailability";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ async function authMiddleware(req: NextApiRequest) {
|
|||||||
if (isSystemWideAdmin) return;
|
if (isSystemWideAdmin) return;
|
||||||
// Find all references where the user has bookings
|
// Find all references where the user has bookings
|
||||||
const bookingReference = await prisma.bookingReference.findFirst({
|
const bookingReference = await prisma.bookingReference.findFirst({
|
||||||
where: { id, booking: { userId } },
|
where: { id, booking: { userId }, deleted: null },
|
||||||
});
|
});
|
||||||
if (!bookingReference) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
if (!bookingReference) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
|||||||
export async function deleteHandler(req: NextApiRequest) {
|
export async function deleteHandler(req: NextApiRequest) {
|
||||||
const { query } = req;
|
const { query } = req;
|
||||||
const { id } = schemaQueryIdParseInt.parse(query);
|
const { id } = schemaQueryIdParseInt.parse(query);
|
||||||
await prisma.bookingReference.delete({ where: { id } });
|
await prisma.bookingReference.update({ where: { id }, data: { deleted: true } });
|
||||||
return { message: `BookingReference with id: ${id} deleted` };
|
return { message: `BookingReference with id: ${id} deleted` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
|||||||
export async function getHandler(req: NextApiRequest) {
|
export async function getHandler(req: NextApiRequest) {
|
||||||
const { query } = req;
|
const { query } = req;
|
||||||
const { id } = schemaQueryIdParseInt.parse(query);
|
const { id } = schemaQueryIdParseInt.parse(query);
|
||||||
const booking_reference = await prisma.bookingReference.findUniqueOrThrow({ where: { id } });
|
const booking_reference = await prisma.bookingReference.findUniqueOrThrow({ where: { id, deleted: null } });
|
||||||
return { booking_reference: schemaBookingReferenceReadPublic.parse(booking_reference) };
|
return { booking_reference: schemaBookingReferenceReadPublic.parse(booking_reference) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ import { schemaBookingReferenceReadPublic } from "~/lib/validations/booking-refe
|
|||||||
* 404:
|
* 404:
|
||||||
* description: No booking references were found
|
* description: No booking references were found
|
||||||
*/
|
*/
|
||||||
async function getHandler(req: NextApiRequest) {
|
export async function handler(req: NextApiRequest) {
|
||||||
const { userId, isSystemWideAdmin } = req;
|
const { userId, isSystemWideAdmin } = req;
|
||||||
const args: Prisma.BookingReferenceFindManyArgs = isSystemWideAdmin
|
const args: Prisma.BookingReferenceFindManyArgs = isSystemWideAdmin
|
||||||
? {}
|
? { where: { deleted: null } }
|
||||||
: { where: { booking: { userId } } };
|
: { where: { booking: { userId }, deleted: null } };
|
||||||
const data = await prisma.bookingReference.findMany(args);
|
const data = await prisma.bookingReference.findMany(args);
|
||||||
return { booking_references: data.map((br) => schemaBookingReferenceReadPublic.parse(br)) };
|
return { booking_references: data.map((br) => schemaBookingReferenceReadPublic.parse(br)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defaultResponder(getHandler);
|
export default defaultResponder(handler);
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
|
||||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
|
||||||
import {
|
import {
|
||||||
getRecordingsOfCalVideoByRoomName,
|
getRecordingsOfCalVideoByRoomName,
|
||||||
getDownloadLinkOfCalVideoByRecordingId,
|
getDownloadLinkOfCalVideoByRecordingId,
|
||||||
} from "@calcom/lib/videoClient";
|
} from "@calcom/features/conferencing/lib/videoClient";
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import type { RecordingItemSchema } from "@calcom/prisma/zod-utils";
|
import type { RecordingItemSchema } from "@calcom/prisma/zod-utils";
|
||||||
import type { PartialReference } from "@calcom/types/EventManager";
|
import type { PartialReference } from "@calcom/types/EventManager";
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
|
||||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
|
||||||
import {
|
import {
|
||||||
getTranscriptsAccessLinkFromRecordingId,
|
getTranscriptsAccessLinkFromRecordingId,
|
||||||
checkIfRoomNameMatchesInRecording,
|
checkIfRoomNameMatchesInRecording,
|
||||||
} from "@calcom/lib/videoClient";
|
} from "@calcom/features/conferencing/lib/videoClient";
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import type { PartialReference } from "@calcom/types/EventManager";
|
import type { PartialReference } from "@calcom/types/EventManager";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
|
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/features/conferencing/lib/videoClient";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/lib/videoClient";
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import type { PartialReference } from "@calcom/types/EventManager";
|
import type { PartialReference } from "@calcom/types/EventManager";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
|
import { getRegularBookingService } from "@calcom/features/bookings/di/RegularBookingService.container";
|
||||||
import getBookingDataSchemaForApi from "@calcom/features/bookings/lib/getBookingDataSchemaForApi";
|
import getBookingDataSchemaForApi from "@calcom/features/bookings/lib/getBookingDataSchemaForApi";
|
||||||
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
|
|
||||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||||
@@ -239,15 +239,17 @@ async function handler(req: NextApiRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await handleNewBooking(
|
const regularBookingService = getRegularBookingService();
|
||||||
{
|
|
||||||
bookingData: req.body,
|
return await regularBookingService.createBookingForApiV1({
|
||||||
|
bookingData: req.body,
|
||||||
|
bookingMeta: {
|
||||||
userId,
|
userId,
|
||||||
hostname: req.headers.host || "",
|
hostname: req.headers.host || "",
|
||||||
forcedSlug: req.headers["x-cal-force-slug"] as string | undefined,
|
forcedSlug: req.headers["x-cal-force-slug"] as string | undefined,
|
||||||
},
|
},
|
||||||
getBookingDataSchemaForApi
|
bookingDataSchemaGetter: getBookingDataSchemaForApi,
|
||||||
);
|
});
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const knownError = error as Error;
|
const knownError = error as Error;
|
||||||
if (knownError?.message === ErrorCode.NoAvailableUsersFound) {
|
if (knownError?.message === ErrorCode.NoAvailableUsersFound) {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
import type { UserWithCalendars } from "@calcom/lib/getConnectedDestinationCalendars";
|
import type { UserWithCalendars } from "@calcom/features/calendars/lib/getConnectedDestinationCalendars";
|
||||||
import { getConnectedDestinationCalendarsAndEnsureDefaultsInDb } from "@calcom/lib/getConnectedDestinationCalendars";
|
import { getConnectedDestinationCalendarsAndEnsureDefaultsInDb } from "@calcom/features/calendars/lib/getConnectedDestinationCalendars";
|
||||||
|
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { extractUserIdsFromQuery } from "~/lib/utils/extractUserIdsFromQuery";
|
import { extractUserIdsFromQuery } from "~/lib/utils/extractUserIdsFromQuery";
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import utc from "dayjs/plugin/utc";
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { getAvailableSlotsService } from "@calcom/features/di/containers/AvailableSlots";
|
||||||
import { isSupportedTimeZone } from "@calcom/lib/dayjs";
|
import { isSupportedTimeZone } from "@calcom/lib/dayjs";
|
||||||
import { getAvailableSlotsService } from "@calcom/lib/di/containers/AvailableSlots";
|
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||||
import { createContext } from "@calcom/trpc/server/createContext";
|
import { createContext } from "@calcom/trpc/server/createContext";
|
||||||
@@ -48,7 +48,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
: availableSlots.slots;
|
: availableSlots.slots;
|
||||||
|
|
||||||
return { slots: slotsInProvidedTimeZone };
|
return { slots: slotsInProvidedTimeZone };
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
if (cause instanceof TRPCError) {
|
if (cause instanceof TRPCError) {
|
||||||
const statusCode = getHTTPStatusCodeFromError(cause);
|
const statusCode = getHTTPStatusCodeFromError(cause);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
import { purchaseTeamOrOrgSubscription } from "@calcom/features/ee/teams/lib/payments";
|
import { purchaseTeamOrOrgSubscription } from "@calcom/features/ee/teams/lib/payments";
|
||||||
|
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
|
||||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||||
@@ -61,24 +62,24 @@ export async function patchHandler(req: NextApiRequest) {
|
|||||||
const { teamId } = schemaQueryTeamId.parse(req.query);
|
const { teamId } = schemaQueryTeamId.parse(req.query);
|
||||||
|
|
||||||
/** Only OWNERS and ADMINS can edit teams */
|
/** Only OWNERS and ADMINS can edit teams */
|
||||||
const _team = await prisma.team.findFirst({
|
const team = await prisma.team.findFirst({
|
||||||
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
// eslint-disable-next-line @calcom/eslint/no-prisma-include-true
|
||||||
include: { members: true },
|
include: { members: true },
|
||||||
where: { id: teamId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
|
where: { id: teamId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
|
||||||
});
|
});
|
||||||
if (!_team) throw new HttpError({ statusCode: 401, message: "Unauthorized: OWNER or ADMIN required" });
|
if (!team) throw new HttpError({ statusCode: 401, message: "Unauthorized: OWNER or ADMIN required" });
|
||||||
|
|
||||||
const slugAlreadyExists = await prisma.team.findFirst({
|
if (data.slug) {
|
||||||
where: {
|
const teamRepository = new TeamRepository(prisma);
|
||||||
slug: {
|
const isSlugAvailable = await teamRepository.isSlugAvailableForUpdate({
|
||||||
mode: "insensitive",
|
slug: data.slug,
|
||||||
equals: data.slug,
|
teamId: team.id,
|
||||||
},
|
parentId: team.parentId,
|
||||||
},
|
});
|
||||||
});
|
if (!isSlugAvailable) {
|
||||||
|
throw new HttpError({ statusCode: 409, message: "Team slug already exists" });
|
||||||
if (slugAlreadyExists && data.slug !== _team.slug)
|
}
|
||||||
throw new HttpError({ statusCode: 409, message: "Team slug already exists" });
|
}
|
||||||
|
|
||||||
// Check if parentId is related to this user
|
// Check if parentId is related to this user
|
||||||
if (data.parentId && data.parentId === teamId) {
|
if (data.parentId && data.parentId === teamId) {
|
||||||
@@ -99,16 +100,16 @@ export async function patchHandler(req: NextApiRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let paymentUrl;
|
let paymentUrl;
|
||||||
if (_team.slug === null && data.slug) {
|
if (team.slug === null && data.slug) {
|
||||||
data.metadata = {
|
data.metadata = {
|
||||||
...(_team.metadata as Prisma.JsonObject),
|
...(team.metadata as Prisma.JsonObject),
|
||||||
requestedSlug: data.slug,
|
requestedSlug: data.slug,
|
||||||
};
|
};
|
||||||
delete data.slug;
|
delete data.slug;
|
||||||
if (IS_TEAM_BILLING_ENABLED) {
|
if (IS_TEAM_BILLING_ENABLED) {
|
||||||
const checkoutSession = await purchaseTeamOrOrgSubscription({
|
const checkoutSession = await purchaseTeamOrOrgSubscription({
|
||||||
teamId: _team.id,
|
teamId: team.id,
|
||||||
seatsUsed: _team.members.length,
|
seatsUsed: team.members.length,
|
||||||
userId,
|
userId,
|
||||||
pricePerSeat: null,
|
pricePerSeat: null,
|
||||||
});
|
});
|
||||||
@@ -131,9 +132,9 @@ export async function patchHandler(req: NextApiRequest) {
|
|||||||
bookingLimits: data.bookingLimits === null ? {} : data.bookingLimits,
|
bookingLimits: data.bookingLimits === null ? {} : data.bookingLimits,
|
||||||
metadata: data.metadata === null ? {} : data.metadata || undefined,
|
metadata: data.metadata === null ? {} : data.metadata || undefined,
|
||||||
};
|
};
|
||||||
const team = await prisma.team.update({ where: { id: teamId }, data: cloneData });
|
const updatedTeam = await prisma.team.update({ where: { id: teamId }, data: cloneData });
|
||||||
const result = {
|
const result = {
|
||||||
team: schemaTeamReadPublic.parse(team),
|
team: schemaTeamReadPublic.parse(updatedTeam),
|
||||||
paymentUrl,
|
paymentUrl,
|
||||||
};
|
};
|
||||||
if (!paymentUrl) {
|
if (!paymentUrl) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
import { deleteUser } from "@calcom/features/users/lib/userDeletionService";
|
import { deleteUser } from "@calcom/features/users/lib/deleteUser";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export async function getHandler(req: NextApiRequest) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const [total, data] = await prisma.$transaction([
|
const [total, data] = await Promise.all([
|
||||||
prisma.user.count({ where }),
|
prisma.user.count({ where }),
|
||||||
prisma.user.findMany({ where, take, skip }),
|
prisma.user.findMany({ where, take, skip }),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
|
import { UserCreationService } from "@calcom/features/users/services/userCreationService";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||||
import { UserCreationService } from "@calcom/lib/server/service/userCreationService";
|
|
||||||
import { CreationSource } from "@calcom/prisma/enums";
|
import { CreationSource } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import { schemaUserCreateBodyParams } from "~/lib/validations/user";
|
import { schemaUserCreateBodyParams } from "~/lib/validations/user";
|
||||||
|
|||||||
@@ -0,0 +1,486 @@
|
|||||||
|
import type { Request, Response } from "express";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { createMocks } from "node-mocks-http";
|
||||||
|
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||||
|
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import type { Booking, Credential, EventType, User } from "@calcom/prisma/client";
|
||||||
|
import { BookingStatus } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
|
import { patchHandler } from "../../pages/api/booking-references/[id]/_patch";
|
||||||
|
import { handler } from "../../pages/api/booking-references/_get";
|
||||||
|
|
||||||
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
|
describe("GET /api/booking-references - Soft-delete filtering", () => {
|
||||||
|
let testUser: User;
|
||||||
|
let testCredential: Credential;
|
||||||
|
let testEventType: EventType;
|
||||||
|
let testBooking: Booking;
|
||||||
|
const createdBookingReferenceIds: number[] = [];
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
testUser = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: "api-bookingreference-test@example.com",
|
||||||
|
username: "api-bookingreference-test",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
testCredential = await prisma.credential.create({
|
||||||
|
data: {
|
||||||
|
type: "google_calendar",
|
||||||
|
key: {},
|
||||||
|
userId: testUser.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
testEventType = await prisma.eventType.create({
|
||||||
|
data: {
|
||||||
|
title: "Test Event Type",
|
||||||
|
slug: "api-test-event-type",
|
||||||
|
length: 30,
|
||||||
|
userId: testUser.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
testBooking = await prisma.booking.create({
|
||||||
|
data: {
|
||||||
|
uid: "api-test-booking-uid",
|
||||||
|
title: "API Test Booking",
|
||||||
|
startTime: new Date(Date.now() + 86400000),
|
||||||
|
endTime: new Date(Date.now() + 90000000),
|
||||||
|
userId: testUser.id,
|
||||||
|
eventTypeId: testEventType.id,
|
||||||
|
status: BookingStatus.ACCEPTED,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (createdBookingReferenceIds.length > 0) {
|
||||||
|
await prisma.bookingReference.deleteMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: createdBookingReferenceIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.booking.delete({
|
||||||
|
where: {
|
||||||
|
id: testBooking.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.eventType.delete({
|
||||||
|
where: {
|
||||||
|
id: testEventType.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.credential.delete({
|
||||||
|
where: {
|
||||||
|
id: testCredential.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.user.delete({
|
||||||
|
where: {
|
||||||
|
id: testUser.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should only return active booking references, excluding soft-deleted ones", async () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const activeRef = await prisma.bookingReference.create({
|
||||||
|
data: {
|
||||||
|
type: "google_calendar",
|
||||||
|
uid: `api-active-ref-${timestamp}`,
|
||||||
|
meetingId: `api-active-meeting-${timestamp}`,
|
||||||
|
credentialId: testCredential.id,
|
||||||
|
bookingId: testBooking.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
createdBookingReferenceIds.push(activeRef.id);
|
||||||
|
|
||||||
|
const deletedRef = await prisma.bookingReference.create({
|
||||||
|
data: {
|
||||||
|
type: "google_calendar",
|
||||||
|
uid: `api-deleted-ref-${timestamp}`,
|
||||||
|
meetingId: `api-deleted-meeting-${timestamp}`,
|
||||||
|
credentialId: testCredential.id,
|
||||||
|
bookingId: testBooking.id,
|
||||||
|
deleted: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
createdBookingReferenceIds.push(deletedRef.id);
|
||||||
|
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = testUser.id;
|
||||||
|
|
||||||
|
const responseData = await handler(req);
|
||||||
|
|
||||||
|
expect(responseData.booking_references).toBeDefined();
|
||||||
|
expect(Array.isArray(responseData.booking_references)).toBe(true);
|
||||||
|
|
||||||
|
const returnedActiveRef = responseData.booking_references.find(
|
||||||
|
(ref: { id: number }) => ref.id === activeRef.id
|
||||||
|
);
|
||||||
|
const returnedDeletedRef = responseData.booking_references.find(
|
||||||
|
(ref: { id: number }) => ref.id === deletedRef.id
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(returnedActiveRef).toBeDefined();
|
||||||
|
expect(returnedDeletedRef).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter booking references by user when not system admin", async () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const otherUser = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: `other-user-bookingreference-${timestamp}@example.com`,
|
||||||
|
username: `other-user-bookingreference-${timestamp}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const otherBooking = await prisma.booking.create({
|
||||||
|
data: {
|
||||||
|
uid: `other-user-booking-uid-${timestamp}`,
|
||||||
|
title: "Other User Booking",
|
||||||
|
startTime: new Date(Date.now() + 86400000),
|
||||||
|
endTime: new Date(Date.now() + 90000000),
|
||||||
|
userId: otherUser.id,
|
||||||
|
eventTypeId: testEventType.id,
|
||||||
|
status: BookingStatus.ACCEPTED,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const otherUserRef = await prisma.bookingReference.create({
|
||||||
|
data: {
|
||||||
|
type: "zoom_video",
|
||||||
|
uid: `other-user-ref-${timestamp}`,
|
||||||
|
meetingId: `other-user-meeting-${timestamp}`,
|
||||||
|
credentialId: testCredential.id,
|
||||||
|
bookingId: otherBooking.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
createdBookingReferenceIds.push(otherUserRef.id);
|
||||||
|
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = testUser.id;
|
||||||
|
|
||||||
|
const responseData = await handler(req);
|
||||||
|
|
||||||
|
const userRefs = responseData.booking_references.filter(
|
||||||
|
(ref: { id: number }) => ref.id === otherUserRef.id
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(userRefs).toHaveLength(0);
|
||||||
|
|
||||||
|
await prisma.booking.delete({ where: { id: otherBooking.id } });
|
||||||
|
await prisma.user.delete({ where: { id: otherUser.id } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return only active booking references for user, not soft-deleted ones for system admin", async () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const activeRef1 = await prisma.bookingReference.create({
|
||||||
|
data: {
|
||||||
|
type: "daily_video",
|
||||||
|
uid: `admin-active-ref-1-${timestamp}`,
|
||||||
|
meetingId: `admin-active-meeting-1-${timestamp}`,
|
||||||
|
credentialId: testCredential.id,
|
||||||
|
bookingId: testBooking.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
createdBookingReferenceIds.push(activeRef1.id);
|
||||||
|
|
||||||
|
const activeRef2 = await prisma.bookingReference.create({
|
||||||
|
data: {
|
||||||
|
type: "daily_video",
|
||||||
|
uid: `admin-active-ref-2-${timestamp}`,
|
||||||
|
meetingId: `admin-active-meeting-2-${timestamp}`,
|
||||||
|
credentialId: testCredential.id,
|
||||||
|
bookingId: testBooking.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
createdBookingReferenceIds.push(activeRef2.id);
|
||||||
|
|
||||||
|
const deletedRef = await prisma.bookingReference.create({
|
||||||
|
data: {
|
||||||
|
type: "daily_video",
|
||||||
|
uid: `admin-deleted-ref-${timestamp}`,
|
||||||
|
meetingId: `admin-deleted-meeting-${timestamp}`,
|
||||||
|
credentialId: testCredential.id,
|
||||||
|
bookingId: testBooking.id,
|
||||||
|
deleted: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
createdBookingReferenceIds.push(deletedRef.id);
|
||||||
|
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = testUser.id;
|
||||||
|
|
||||||
|
const responseData = await handler(req);
|
||||||
|
|
||||||
|
const activeRefs = responseData.booking_references.filter(
|
||||||
|
(ref: { id: number }) => ref.id === activeRef1.id || ref.id === activeRef2.id
|
||||||
|
);
|
||||||
|
const returnedDeletedRef = responseData.booking_references.find(
|
||||||
|
(ref: { id: number }) => ref.id === deletedRef.id
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(activeRefs.length).toBe(2);
|
||||||
|
expect(returnedDeletedRef).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should verify soft-deleted references are never returned through the API", async () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const activeRef = await prisma.bookingReference.create({
|
||||||
|
data: {
|
||||||
|
type: "office365_calendar",
|
||||||
|
uid: `never-return-active-${timestamp}`,
|
||||||
|
meetingId: `never-return-active-meeting-${timestamp}`,
|
||||||
|
credentialId: testCredential.id,
|
||||||
|
bookingId: testBooking.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
createdBookingReferenceIds.push(activeRef.id);
|
||||||
|
|
||||||
|
await prisma.bookingReference.update({
|
||||||
|
where: { id: activeRef.id },
|
||||||
|
data: { deleted: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = testUser.id;
|
||||||
|
|
||||||
|
const responseData = await handler(req);
|
||||||
|
|
||||||
|
const softDeletedRef = responseData.booking_references.find(
|
||||||
|
(ref: { id: number }) => ref.id === activeRef.id
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(softDeletedRef).toBeUndefined();
|
||||||
|
|
||||||
|
const refInDb = await prisma.bookingReference.findUnique({
|
||||||
|
where: { id: activeRef.id },
|
||||||
|
});
|
||||||
|
expect(refInDb).toBeDefined();
|
||||||
|
expect(refInDb?.deleted).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("PATCH /api/booking-references/[id] - Existing functionality", () => {
|
||||||
|
let testUser: User;
|
||||||
|
let testCredential: Credential;
|
||||||
|
let testEventType: EventType;
|
||||||
|
let testBooking: Booking;
|
||||||
|
const createdBookingReferenceIds: number[] = [];
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
testUser = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: "api-bookingreference-patch-test@example.com",
|
||||||
|
username: "api-bookingreference-patch-test",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
testCredential = await prisma.credential.create({
|
||||||
|
data: {
|
||||||
|
type: "google_calendar",
|
||||||
|
key: {},
|
||||||
|
userId: testUser.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
testEventType = await prisma.eventType.create({
|
||||||
|
data: {
|
||||||
|
title: "Test Event Type",
|
||||||
|
slug: "api-patch-test-event-type",
|
||||||
|
length: 30,
|
||||||
|
userId: testUser.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
testBooking = await prisma.booking.create({
|
||||||
|
data: {
|
||||||
|
uid: "api-patch-test-booking-uid",
|
||||||
|
title: "API Patch Test Booking",
|
||||||
|
startTime: new Date(Date.now() + 86400000),
|
||||||
|
endTime: new Date(Date.now() + 90000000),
|
||||||
|
userId: testUser.id,
|
||||||
|
eventTypeId: testEventType.id,
|
||||||
|
status: BookingStatus.ACCEPTED,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (createdBookingReferenceIds.length > 0) {
|
||||||
|
await prisma.bookingReference.deleteMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: createdBookingReferenceIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.booking.delete({
|
||||||
|
where: {
|
||||||
|
id: testBooking.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.eventType.delete({
|
||||||
|
where: {
|
||||||
|
id: testEventType.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.credential.delete({
|
||||||
|
where: {
|
||||||
|
id: testCredential.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.user.delete({
|
||||||
|
where: {
|
||||||
|
id: testUser.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully update an active booking reference", async () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const activeRef = await prisma.bookingReference.create({
|
||||||
|
data: {
|
||||||
|
type: "google_calendar",
|
||||||
|
uid: `patch-active-ref-${timestamp}`,
|
||||||
|
meetingId: `patch-active-meeting-${timestamp}`,
|
||||||
|
credentialId: testCredential.id,
|
||||||
|
bookingId: testBooking.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
createdBookingReferenceIds.push(activeRef.id);
|
||||||
|
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "PATCH",
|
||||||
|
query: { id: activeRef.id.toString() },
|
||||||
|
body: {
|
||||||
|
meetingPassword: "new-password-123",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = testUser.id;
|
||||||
|
req.isSystemWideAdmin = false;
|
||||||
|
|
||||||
|
const responseData = await patchHandler(req);
|
||||||
|
|
||||||
|
expect(responseData.booking_reference).toBeDefined();
|
||||||
|
expect(responseData.booking_reference.id).toBe(activeRef.id);
|
||||||
|
expect(responseData.booking_reference.meetingPassword).toBe("new-password-123");
|
||||||
|
|
||||||
|
const updatedRef = await prisma.bookingReference.findUnique({
|
||||||
|
where: { id: activeRef.id },
|
||||||
|
});
|
||||||
|
expect(updatedRef?.meetingPassword).toBe("new-password-123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow updating a soft-deleted booking reference (existing functionality)", async () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const deletedRef = await prisma.bookingReference.create({
|
||||||
|
data: {
|
||||||
|
type: "google_calendar",
|
||||||
|
uid: `patch-deleted-ref-${timestamp}`,
|
||||||
|
meetingId: `patch-deleted-meeting-${timestamp}`,
|
||||||
|
credentialId: testCredential.id,
|
||||||
|
bookingId: testBooking.id,
|
||||||
|
deleted: true,
|
||||||
|
meetingPassword: "original-password",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
createdBookingReferenceIds.push(deletedRef.id);
|
||||||
|
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "PATCH",
|
||||||
|
query: { id: deletedRef.id.toString() },
|
||||||
|
body: {
|
||||||
|
meetingPassword: "updated-password",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = testUser.id;
|
||||||
|
req.isSystemWideAdmin = false;
|
||||||
|
|
||||||
|
const responseData = await patchHandler(req);
|
||||||
|
|
||||||
|
expect(responseData.booking_reference).toBeDefined();
|
||||||
|
expect(responseData.booking_reference.id).toBe(deletedRef.id);
|
||||||
|
expect(responseData.booking_reference.meetingPassword).toBe("updated-password");
|
||||||
|
|
||||||
|
const updatedRef = await prisma.bookingReference.findUnique({
|
||||||
|
where: { id: deletedRef.id },
|
||||||
|
});
|
||||||
|
expect(updatedRef?.meetingPassword).toBe("updated-password");
|
||||||
|
expect(updatedRef?.deleted).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should verify that booking references can be updated regardless of deleted status", async () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const activeRef = await prisma.bookingReference.create({
|
||||||
|
data: {
|
||||||
|
type: "zoom_video",
|
||||||
|
uid: `patch-verify-active-${timestamp}`,
|
||||||
|
meetingId: `patch-verify-active-meeting-${timestamp}`,
|
||||||
|
credentialId: testCredential.id,
|
||||||
|
bookingId: testBooking.id,
|
||||||
|
meetingPassword: "original-password",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
createdBookingReferenceIds.push(activeRef.id);
|
||||||
|
|
||||||
|
await prisma.bookingReference.update({
|
||||||
|
where: { id: activeRef.id },
|
||||||
|
data: { deleted: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "PATCH",
|
||||||
|
query: { id: activeRef.id.toString() },
|
||||||
|
body: {
|
||||||
|
meetingPassword: "updated-password",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = testUser.id;
|
||||||
|
req.isSystemWideAdmin = false;
|
||||||
|
|
||||||
|
const responseData = await patchHandler(req);
|
||||||
|
|
||||||
|
expect(responseData.booking_reference).toBeDefined();
|
||||||
|
expect(responseData.booking_reference.meetingPassword).toBe("updated-password");
|
||||||
|
|
||||||
|
const refInDb = await prisma.bookingReference.findUnique({
|
||||||
|
where: { id: activeRef.id },
|
||||||
|
});
|
||||||
|
expect(refInDb?.meetingPassword).toBe("updated-password");
|
||||||
|
expect(refInDb?.deleted).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { createMocks } from "node-mocks-http";
|
import { createMocks } from "node-mocks-http";
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect, beforeAll } from "vitest";
|
||||||
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
@@ -11,6 +11,30 @@ type CustomNextApiRequest = NextApiRequest & Request;
|
|||||||
type CustomNextApiResponse = NextApiResponse & Response;
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
describe("PATCH /api/bookings", () => {
|
describe("PATCH /api/bookings", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
const acmeOrg = await prisma.team.findFirst({
|
||||||
|
where: {
|
||||||
|
slug: "acme",
|
||||||
|
isOrganization: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (acmeOrg) {
|
||||||
|
await prisma.organizationSettings.upsert({
|
||||||
|
where: {
|
||||||
|
organizationId: acmeOrg.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
isAdminAPIEnabled: true,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
organizationId: acmeOrg.id,
|
||||||
|
orgAutoAcceptEmail: "acme.com",
|
||||||
|
isAdminAPIEnabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
it("Returns 403 when user has no permission to the booking", async () => {
|
it("Returns 403 when user has no permission to the booking", async () => {
|
||||||
const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } });
|
const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } });
|
||||||
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||||||
import { createMocks } from "node-mocks-http";
|
import { createMocks } from "node-mocks-http";
|
||||||
import { describe, expect, test, vi, afterEach } from "vitest";
|
import { describe, expect, test, vi, afterEach } from "vitest";
|
||||||
|
|
||||||
import { buildBooking } from "@calcom/lib/test/builder";
|
|
||||||
import {
|
import {
|
||||||
getRecordingsOfCalVideoByRoomName,
|
getRecordingsOfCalVideoByRoomName,
|
||||||
getDownloadLinkOfCalVideoByRecordingId,
|
getDownloadLinkOfCalVideoByRecordingId,
|
||||||
} from "@calcom/lib/videoClient";
|
} from "@calcom/features/conferencing/lib/videoClient";
|
||||||
|
import { buildBooking } from "@calcom/lib/test/builder";
|
||||||
|
|
||||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ type CustomNextApiResponse = NextApiResponse & Response;
|
|||||||
const adminUserId = 1;
|
const adminUserId = 1;
|
||||||
const memberUserId = 10;
|
const memberUserId = 10;
|
||||||
|
|
||||||
vi.mock("@calcom/lib/videoClient", () => {
|
vi.mock("@calcom/features/conferencing/lib/videoClient", () => {
|
||||||
return {
|
return {
|
||||||
getRecordingsOfCalVideoByRoomName: vi.fn(),
|
getRecordingsOfCalVideoByRoomName: vi.fn(),
|
||||||
getDownloadLinkOfCalVideoByRecordingId: vi.fn(),
|
getDownloadLinkOfCalVideoByRecordingId: vi.fn(),
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||||||
import { createMocks } from "node-mocks-http";
|
import { createMocks } from "node-mocks-http";
|
||||||
import { describe, expect, test, vi, afterEach } from "vitest";
|
import { describe, expect, test, vi, afterEach } from "vitest";
|
||||||
|
|
||||||
import { buildBooking } from "@calcom/lib/test/builder";
|
|
||||||
import {
|
import {
|
||||||
getTranscriptsAccessLinkFromRecordingId,
|
getTranscriptsAccessLinkFromRecordingId,
|
||||||
checkIfRoomNameMatchesInRecording,
|
checkIfRoomNameMatchesInRecording,
|
||||||
} from "@calcom/lib/videoClient";
|
} from "@calcom/features/conferencing/lib/videoClient";
|
||||||
|
import { buildBooking } from "@calcom/lib/test/builder";
|
||||||
|
|
||||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ import handler from "../../../../../../pages/api/bookings/[id]/transcripts/[reco
|
|||||||
type CustomNextApiRequest = NextApiRequest & Request;
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
type CustomNextApiResponse = NextApiResponse & Response;
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
vi.mock("@calcom/lib/videoClient", () => {
|
vi.mock("@calcom/features/conferencing/lib/videoClient", () => {
|
||||||
return {
|
return {
|
||||||
getTranscriptsAccessLinkFromRecordingId: vi.fn(),
|
getTranscriptsAccessLinkFromRecordingId: vi.fn(),
|
||||||
checkIfRoomNameMatchesInRecording: vi.fn(),
|
checkIfRoomNameMatchesInRecording: vi.fn(),
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||||||
import { createMocks } from "node-mocks-http";
|
import { createMocks } from "node-mocks-http";
|
||||||
import { describe, expect, test, vi, afterEach } from "vitest";
|
import { describe, expect, test, vi, afterEach } from "vitest";
|
||||||
|
|
||||||
|
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/features/conferencing/lib/videoClient";
|
||||||
import { buildBooking } from "@calcom/lib/test/builder";
|
import { buildBooking } from "@calcom/lib/test/builder";
|
||||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/lib/videoClient";
|
|
||||||
|
|
||||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ import handler from "../../../../../pages/api/bookings/[id]/transcripts/_get";
|
|||||||
type CustomNextApiRequest = NextApiRequest & Request;
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
type CustomNextApiResponse = NextApiResponse & Response;
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
vi.mock("@calcom/lib/videoClient", () => {
|
vi.mock("@calcom/features/conferencing/lib/videoClient", () => {
|
||||||
return {
|
return {
|
||||||
getAllTranscriptsAccessLinkFromRoomName: vi.fn(),
|
getAllTranscriptsAccessLinkFromRoomName: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { createMocks } from "node-mocks-http";
|
import { createMocks } from "node-mocks-http";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it, beforeAll } from "vitest";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
import prisma from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
|
|
||||||
import { handler } from "../../../pages/api/bookings/_get";
|
import { handler } from "../../../pages/api/bookings/_get";
|
||||||
|
|
||||||
@@ -17,6 +17,30 @@ const DefaultPagination = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("GET /api/bookings", async () => {
|
describe("GET /api/bookings", async () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
const acmeOrg = await prisma.team.findFirst({
|
||||||
|
where: {
|
||||||
|
slug: "acme",
|
||||||
|
isOrganization: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (acmeOrg) {
|
||||||
|
await prisma.organizationSettings.upsert({
|
||||||
|
where: {
|
||||||
|
organizationId: acmeOrg.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
isAdminAPIEnabled: true,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
organizationId: acmeOrg.id,
|
||||||
|
orgAutoAcceptEmail: "acme.com",
|
||||||
|
isAdminAPIEnabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
||||||
const proUserBooking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
|
const proUserBooking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
|
||||||
|
|
||||||
@@ -193,6 +217,22 @@ describe("GET /api/bookings", async () => {
|
|||||||
describe("Expand feature to add relational data in return payload", () => {
|
describe("Expand feature to add relational data in return payload", () => {
|
||||||
it("Returns only team data when expand=team is set", async () => {
|
it("Returns only team data when expand=team is set", async () => {
|
||||||
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
|
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
|
||||||
|
|
||||||
|
// Find a team booking and a non-team booking from seed data
|
||||||
|
const team1 = await prisma.team.findFirst({ where: { slug: "team1" } });
|
||||||
|
const teamEventType = await prisma.eventType.findFirst({
|
||||||
|
where: { teamId: team1?.id },
|
||||||
|
include: { bookings: true },
|
||||||
|
});
|
||||||
|
const teamBooking = teamEventType?.bookings[0];
|
||||||
|
|
||||||
|
const nonTeamBooking = await prisma.booking.findFirst({
|
||||||
|
where: {
|
||||||
|
eventType: { teamId: null },
|
||||||
|
userId: proUser.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
query: {
|
query: {
|
||||||
@@ -205,14 +245,23 @@ describe("GET /api/bookings", async () => {
|
|||||||
req.isOrganizationOwnerOrAdmin = true;
|
req.isOrganizationOwnerOrAdmin = true;
|
||||||
|
|
||||||
const responseData = await handler(req);
|
const responseData = await handler(req);
|
||||||
console.log("bookings=>", responseData.bookings);
|
|
||||||
responseData.bookings.forEach((booking) => {
|
// Verify team booking has team data
|
||||||
if (booking.id === 31) expect(booking.eventType?.team?.slug).toBe("team1");
|
if (teamBooking) {
|
||||||
if (booking.id === 19) {
|
const returnedTeamBooking = responseData.bookings.find((b) => b.id === teamBooking.id);
|
||||||
// The team field can be either null or undefined due to nullish() in the schema
|
expect(returnedTeamBooking?.eventType?.team?.slug).toBe("team1");
|
||||||
expect(booking.eventType?.team === null || booking.eventType?.team === undefined).toBe(true);
|
}
|
||||||
|
|
||||||
|
// Verify non-team booking has null/undefined team
|
||||||
|
if (nonTeamBooking) {
|
||||||
|
const returnedNonTeamBooking = responseData.bookings.find((b) => b.id === nonTeamBooking.id);
|
||||||
|
if (returnedNonTeamBooking) {
|
||||||
|
expect(
|
||||||
|
returnedNonTeamBooking.eventType?.team === null ||
|
||||||
|
returnedNonTeamBooking.eventType?.team === undefined
|
||||||
|
).toBe(true);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -318,11 +367,6 @@ describe("GET /api/bookings", async () => {
|
|||||||
|
|
||||||
const testUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
const testUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
||||||
|
|
||||||
const testUserBooking = await prisma.booking.findFirstOrThrow({
|
|
||||||
where: { userId: testUser.id },
|
|
||||||
include: { attendees: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
query: {
|
query: {
|
||||||
|
|||||||
Loaded 100 of 2490 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user