Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
782127a993 | ||
|
|
fbc3f7b51f | ||
|
|
619ccf4065 | ||
|
|
40d8f34e8d | ||
|
|
9c1e1d7312 | ||
|
|
710a1a7d38 | ||
|
|
f25605ef4d | ||
|
|
684575d0a4 | ||
|
|
bc6267e99b | ||
|
|
9901c91e9b | ||
|
|
dfe03efc37 | ||
|
|
0eabb56f07 | ||
|
|
cdd066c653 | ||
|
|
026f22fbaf | ||
|
|
5950c5a756 | ||
|
|
cd9d16be3e | ||
|
|
993f92acba | ||
|
|
6f110d21fd | ||
|
|
0df6777814 | ||
|
|
5f14cd31d1 | ||
|
|
070ec326aa | ||
|
|
4ca79af13f | ||
|
|
a2e70f9aad | ||
|
|
f186d2e41d | ||
|
|
aaeea250bc | ||
|
|
96af17d8d7 | ||
|
|
dfaa6d28e4 | ||
|
|
6a2de993bb | ||
|
|
b69d885ee6 | ||
|
|
d001c4e2ae | ||
|
|
558f1c9478 | ||
|
|
74748a6183 | ||
|
|
986f17f54b | ||
|
|
ab342016d2 | ||
|
|
c34c27fc0a | ||
|
|
6dbf372ab0 | ||
|
|
7dc7f949cf | ||
|
|
5690718e7f | ||
|
|
97d5bb9fe6 | ||
|
|
fcc50c1d0f | ||
|
|
b832289f8e | ||
|
|
4aa0b4cc65 | ||
|
|
e2ef9dd710 | ||
|
|
15b0e0c5bd | ||
|
|
020515de8c | ||
|
|
87cb8e918f | ||
|
|
69677495ca | ||
|
|
a40520b90c | ||
|
|
c07b456b2d | ||
|
|
7ec2a4b8fa | ||
|
|
dccaf5fb67 | ||
|
|
d76060e037 | ||
|
|
48d4725e10 | ||
|
|
2f1e545976 | ||
|
|
50fb903ba6 | ||
|
|
ccda3fd71e | ||
|
|
c44fb074ab | ||
|
|
d56d649bf0 | ||
|
|
8856ba7d70 | ||
|
|
0897bf20da | ||
|
|
6ce6d570db | ||
|
|
b99ccb1a5a | ||
|
|
698d8ae4bd | ||
|
|
0dddc2224a | ||
|
|
49f9f5489b | ||
|
|
ecb693c70e | ||
|
|
3c6fdfe724 | ||
|
|
ef7f0e2259 | ||
|
|
3791af8644 | ||
|
|
c19799e275 | ||
|
|
4c4fc9e38b | ||
|
|
2220778e6b | ||
|
|
0f707a55b0 | ||
|
|
de1c9d01cd | ||
|
|
6a1325867e | ||
|
|
574a4a847d | ||
|
|
f201266d69 | ||
|
|
71e57bafde | ||
|
|
c4b296d580 | ||
|
|
7d7e74c869 | ||
|
|
6dec98129c | ||
|
|
c7b62950de | ||
|
|
4dba72fecb | ||
|
|
e61be783d1 | ||
|
|
299a866aac | ||
|
|
e39e6ccf79 | ||
|
|
218f6a84b9 | ||
|
|
1a3e4d10c0 | ||
|
|
f848a44f1a | ||
|
|
2181731d64 | ||
|
|
8c8401330a | ||
|
|
7015c8909f | ||
|
|
cbee4ff704 | ||
|
|
ffefb3461e | ||
|
|
1c2fff5447 | ||
|
|
6f942cfcac | ||
|
|
7579417d7e | ||
|
|
16d1adf990 | ||
|
|
bdc36acdf5 | ||
|
|
1f036bf35e | ||
|
|
5de77e386c | ||
|
|
412e7ecbce | ||
|
|
55c9efec3e | ||
|
|
c4792c55fe | ||
|
|
04cad3a69e | ||
|
|
f7b257750a | ||
|
|
4fa7bb64eb | ||
|
|
69656b7861 | ||
|
|
b543c4030c | ||
|
|
4ce62b84ce | ||
|
|
076868d243 | ||
|
|
a03a1ba34e | ||
|
|
dea873fef1 | ||
|
|
9fbb0808be | ||
|
|
beae1aae8b | ||
|
|
e56b4b57bf | ||
|
|
4faad64978 | ||
|
|
b384e4a9d8 | ||
|
|
d07e86e4f3 | ||
|
|
68d40cabbe | ||
|
|
f07d0479d4 | ||
|
|
9a2c2aaca4 | ||
|
|
2693b30c51 | ||
|
|
95cc0eeaae | ||
|
|
b49317a83f | ||
|
|
24264c32dc | ||
|
|
e248dfbda7 | ||
|
|
31c0804386 | ||
|
|
a3443b4e76 | ||
|
|
254dfc9e6a | ||
|
|
ef6bf3d451 | ||
|
|
4b3ad579c8 | ||
|
|
27ce5e7808 | ||
|
|
4d99a7fbdf | ||
|
|
27541227c8 | ||
|
|
83a187bf11 | ||
|
|
200ce6932d | ||
|
|
6d5983fabc | ||
|
|
18b994bb4c | ||
|
|
a8975f541f | ||
|
|
31b88c5537 | ||
|
|
f0bd628713 | ||
|
|
260bd4829c | ||
|
|
dc8731d64f | ||
|
|
48eba5ef5c | ||
|
|
544d76d503 | ||
|
|
6ce2d9819e | ||
|
|
2e0c9c7068 | ||
|
|
038008502c | ||
|
|
5886792285 | ||
|
|
e3905f631f | ||
|
|
e5e0fa97eb | ||
|
|
f1caaad536 | ||
|
|
b6279f4128 | ||
|
|
5fb1857538 | ||
|
|
a8d9b0210a | ||
|
|
5751f0f634 | ||
|
|
4b16860d07 | ||
|
|
f77e480d02 | ||
|
|
099f36deeb | ||
|
|
09f804413b | ||
|
|
e1a9576530 | ||
|
|
2410f8b3aa | ||
|
|
a28e8ff39b | ||
|
|
7d2500a32f | ||
|
|
22d906798e | ||
|
|
f71759f70b | ||
|
|
a323e06e88 | ||
|
|
b4db3a75a8 | ||
|
|
a9fcd900cd | ||
|
|
fcb443a8eb | ||
|
|
9dfa596e3e | ||
|
|
0e8ac7e4ed | ||
|
|
b236bffca2 | ||
|
|
6e829e23f9 | ||
|
|
b778b2962a | ||
|
|
0d3baee593 | ||
|
|
f9dcbaaa42 | ||
|
|
60700e1a7a | ||
|
|
456adea570 | ||
|
|
63abda1693 | ||
|
|
4dd73f4d86 | ||
|
|
31caf8288d | ||
|
|
99e71365ab | ||
|
|
943c7a4c6c | ||
|
|
228e509a3b | ||
|
|
eb909cc87a | ||
|
|
65a36b37c5 | ||
|
|
2799ddf3a4 | ||
|
|
3164cd4ae7 | ||
|
|
6b26dbc6da | ||
|
|
6b09c6b3b4 | ||
|
|
2352bb1075 | ||
|
|
a568e1ac66 | ||
|
|
add6ffdfc4 | ||
|
|
7a67331d96 | ||
|
|
1aa8b8439a | ||
|
|
50e405353c | ||
|
|
565e21bc50 | ||
|
|
098d41fc62 | ||
|
|
a20179727b | ||
|
|
795aaca64f | ||
|
|
2ab7846a4a | ||
|
|
e9ea0fcc20 | ||
|
|
090b166b1c | ||
|
|
ea2d8fc873 | ||
|
|
46c51df1e0 | ||
|
|
5fe919452b | ||
|
|
83c8f97afd | ||
|
|
f87eac193f | ||
|
|
c53e891c8a | ||
|
|
90a6fc3f26 | ||
|
|
969411041b | ||
|
|
518cfbc037 | ||
|
|
47277ced2d | ||
|
|
db625157d1 | ||
|
|
bf2af799d5 | ||
|
|
75eaed1c4d | ||
|
|
e1ac6f5454 | ||
|
|
55d44ce789 | ||
|
|
8bed690ed6 | ||
|
|
adf012643a | ||
|
|
ca0d23529a | ||
|
|
9f50941904 | ||
|
|
f2a59fe4e8 | ||
|
|
325f250d39 | ||
|
|
bb7057ea04 | ||
|
|
d185d7f7d8 | ||
|
|
80b92b5f11 | ||
|
|
9a6d4e63e8 | ||
|
|
4062ae8486 | ||
|
|
c6f26f2a2a | ||
|
|
2f4b1818d0 | ||
|
|
8e1ce633fb | ||
|
|
44e4f4c644 | ||
|
|
0b9cf78e9b | ||
|
|
b2cdb08780 | ||
|
|
dcf9c6a5e5 | ||
|
|
ca78be011c | ||
|
|
d13dedda9a | ||
|
|
9ca2ad2d41 | ||
|
|
3d89809e67 | ||
|
|
252ce07861 | ||
|
|
a75ef8870b | ||
|
|
b840b3b1bb | ||
|
|
2e50625165 | ||
|
|
c78eb752a7 | ||
|
|
1c20bdcecf | ||
|
|
338756439f | ||
|
|
56050b994d | ||
|
|
c11ecbb323 | ||
|
|
7f23ae156b | ||
|
|
33250652b3 | ||
|
|
a65e18d92d | ||
|
|
d0f7085cb8 | ||
|
|
ea8437b4f7 | ||
|
|
877cd4cdff | ||
|
|
bae3bd76e5 | ||
|
|
8b24995d52 | ||
|
|
7dc6df2ad6 | ||
|
|
6c4b1154b8 | ||
|
|
1099bad930 | ||
|
|
dbd5d27d9d | ||
|
|
45c4f4789b | ||
|
|
07f42057fc | ||
|
|
79ee7c41b2 | ||
|
|
57e6971942 | ||
|
|
bd6ca21e02 | ||
|
|
f19a1926b4 | ||
|
|
477ee1c526 | ||
|
|
6704e18e32 | ||
|
|
20f17903b9 | ||
|
|
8dabbbd09f | ||
|
|
32933ed6ef | ||
|
|
c765ea9b31 | ||
|
|
d3ddbf418f | ||
|
|
79d0513428 | ||
|
|
230b82d3bf | ||
|
|
c9b50ffb78 | ||
|
|
ada0ef242b | ||
|
|
b26e688950 | ||
|
|
d9041d03b7 | ||
|
|
f60b3c14e1 | ||
|
|
fbcc43931e | ||
|
|
a48e7cb39a | ||
|
|
db3f718ba6 | ||
|
|
ca5dd4626d | ||
|
|
4524d722f6 | ||
|
|
2094d59856 | ||
|
|
7b350a5b8f | ||
|
|
5a869228d3 | ||
|
|
dabd5eae73 | ||
|
|
cb78de231a | ||
|
|
b0a1ef8a49 | ||
|
|
0a43aa3351 | ||
|
|
ed4cce1c7e | ||
|
|
d595aa23c0 | ||
|
|
a6df49235f | ||
|
|
5df41e37a0 | ||
|
|
fca5778b6d | ||
|
|
0ddefaa141 | ||
|
|
c11f7aeffc | ||
|
|
9903fcaa05 | ||
|
|
0910f65b87 | ||
|
|
343f8ee303 | ||
|
|
de479bb2da | ||
|
|
5dc3065a47 | ||
|
|
36d315343c | ||
|
|
2171a320f5 | ||
|
|
7ee035c3a3 | ||
|
|
1de05ebf3d | ||
|
|
53d7e2ec3f | ||
|
|
f65c7e413f | ||
|
|
b762f60214 | ||
|
|
2498785c49 | ||
|
|
9a6683e01d | ||
|
|
2853288f49 | ||
|
|
d04226ab9a | ||
|
|
cb7ddc455a | ||
|
|
73aa1e8a22 | ||
|
|
a3b5263b76 | ||
|
|
af2c6c0844 | ||
|
|
113195224a | ||
|
|
828092c1d0 | ||
|
|
4b060fc2cd | ||
|
|
16c5b070b6 | ||
|
|
48dde246e9 | ||
|
|
556b382f75 | ||
|
|
85237c4985 | ||
|
|
404bc0e4d6 | ||
|
|
00553e897b | ||
|
|
ed2ce005c9 | ||
|
|
058844b6b8 | ||
|
|
77fa3702db | ||
|
|
9eb2ae470c | ||
|
|
3de7b73f5f | ||
|
|
8e7785ca64 | ||
|
|
608b891536 | ||
|
|
4f26ca1a7b | ||
|
|
c55b36f235 | ||
|
|
bdd3b132d4 | ||
|
|
9a4c20cca4 | ||
|
|
2144fcb23e | ||
|
|
f2c39fc786 | ||
|
|
e83e0a770c | ||
|
|
c818ef3188 | ||
|
|
176564f4ed | ||
|
|
27d59b6413 | ||
|
|
57a65401e7 | ||
|
|
28acbe549a | ||
|
|
3534e3c224 | ||
|
|
e48a6c3dcf | ||
|
|
7d09ccb0d7 | ||
|
|
9029c5b4c4 | ||
|
|
a4c1df3658 | ||
|
|
0b96ef5476 | ||
|
|
ca9e0f5b72 | ||
|
|
a9418b06d6 | ||
|
|
833a326c5b | ||
|
|
b7e2a62e26 | ||
|
|
f687056d60 | ||
|
|
0a39f53a4b | ||
|
|
ea0a64624c | ||
|
|
6a8726f5f8 | ||
|
|
eb97e1660b | ||
|
|
371a0f7245 | ||
|
|
270d4f6e82 | ||
|
|
48e7b616b8 | ||
|
|
8b80ea99c2 | ||
|
|
150c6e0f50 | ||
|
|
4037e87fbd | ||
|
|
a804a29516 | ||
|
|
87b514b91b | ||
|
|
693ca04626 | ||
|
|
67f83c7e7d | ||
|
|
ecfecd832d | ||
|
|
e55302cbc5 | ||
|
|
9514a68039 | ||
|
|
4e3d159401 | ||
|
|
a519941b81 | ||
|
|
f62c58532d | ||
|
|
0981c4cb23 | ||
|
|
9d06f6dd0e | ||
|
|
00df493623 | ||
|
|
2cf2b11a7b | ||
|
|
5c3fd0993e | ||
|
|
811e655720 | ||
|
|
defaafc9be | ||
|
|
e0c6feff1f | ||
|
|
d17dcbd319 | ||
|
|
90d52663ac | ||
|
|
ada74467bc | ||
|
|
7db758c2d5 | ||
|
|
2867d53442 | ||
|
|
31067fcd45 | ||
|
|
8c2ce972cc | ||
|
|
d3ab11e38e | ||
|
|
3d596fcb1a | ||
|
|
820b45a912 | ||
|
|
b945849736 | ||
|
|
6b2c59b733 | ||
|
|
b7f85d785a | ||
|
|
08e4e816c6 | ||
|
|
20430022e1 | ||
|
|
e4c71e5d2c | ||
|
|
28eda407be | ||
|
|
9dab171520 | ||
|
|
d5f359291d | ||
|
|
3c1d0a9bf1 | ||
|
|
3057edbd87 | ||
|
|
a2f859b55a | ||
|
|
b2f4eaee63 | ||
|
|
b66c3f3452 | ||
|
|
5ab0b85e52 | ||
|
|
4135c1038e | ||
|
|
fc5190a33b | ||
|
|
f29328e6a2 | ||
|
|
621f7639ff | ||
|
|
63f2f6edba | ||
|
|
199f41fb1a | ||
|
|
3ea9faa414 | ||
|
|
518ec4f605 | ||
|
|
4db0da5da4 | ||
|
|
115ded8429 | ||
|
|
71485d1b57 | ||
|
|
87071c4f19 | ||
|
|
412f15814e | ||
|
|
a57085cf64 | ||
|
|
65d1a34e39 | ||
|
|
e4abc85382 | ||
|
|
eb1eb722f2 | ||
|
|
033bc45dd1 | ||
|
|
affe85d02c | ||
|
|
d6f9a833bd | ||
|
|
e4281a852a | ||
|
|
e4b2464548 | ||
|
|
8c99a2a7af | ||
|
|
2236dfe411 | ||
|
|
a67369460a | ||
|
|
b2526300a7 | ||
|
|
d69395434f | ||
|
|
2720c9cc67 | ||
|
|
f1465a15f6 | ||
|
|
1d24f2654d | ||
|
|
f7eb86577c | ||
|
|
cc515f00b7 | ||
|
|
e695f75113 | ||
|
|
0d1b6ea5b2 | ||
|
|
26637cda08 | ||
|
|
529054aa76 | ||
|
|
8deee738c2 | ||
|
|
6848362683 | ||
|
|
09fc7e1a4c | ||
|
|
2f73be0ee6 | ||
|
|
3cdf59f2cc | ||
|
|
1f9880204e | ||
|
|
d0be48ebde | ||
|
|
5c8abbb3e9 | ||
|
|
7bb42ba577 | ||
|
|
2e3b3257db | ||
|
|
d28e20e92e | ||
|
|
8b89d6635a | ||
|
|
13ec810cb9 | ||
|
|
367c8a96d8 | ||
|
|
e94c030c45 | ||
|
|
6d0c7e4214 | ||
|
|
e11cb9e3a5 | ||
|
|
809abd2f4f | ||
|
|
d1d50b0d91 | ||
|
|
fc716f5921 | ||
|
|
5348464972 | ||
|
|
3fac6e698e | ||
|
|
dcafbf672a | ||
|
|
dff89a4df8 | ||
|
|
ed04062540 | ||
|
|
23fda4a9e6 | ||
|
|
14c3028c2a | ||
|
|
dfe49e1732 | ||
|
|
6148c6baea | ||
|
|
5366d891bb | ||
|
|
2ee021a87f | ||
|
|
dd0c2dbff2 | ||
|
|
f044c2d7c7 | ||
|
|
703bcd861e | ||
|
|
d4d96eb5cd | ||
|
|
d8356e9a5b | ||
|
|
9348279818 | ||
|
|
5d77e4ce1d | ||
|
|
968e782dbe | ||
|
|
01fb3dd03b | ||
|
|
c3537f3290 | ||
|
|
3bb42f276e | ||
|
|
51fd4102ae | ||
|
|
9d1ef0a649 | ||
|
|
f80dc0738a | ||
|
|
678ab3f453 | ||
|
|
199d3e4c3f | ||
|
|
79a6aef0e7 | ||
|
|
4d49fb0636 | ||
|
|
0be1387d0f | ||
|
|
58ab278813 | ||
|
|
4de142cb7c | ||
|
|
b4d27a9326 | ||
|
|
31f3d9778e | ||
|
|
0a59c95b93 | ||
|
|
9e3465eeb6 | ||
|
|
31fc4724e0 | ||
|
|
f81f0a26ec | ||
|
|
9a80bb6194 | ||
|
|
901fc36c97 | ||
|
|
2831fb2b57 | ||
|
|
426d31712e | ||
|
|
09ecd445bb | ||
|
|
08d65c85de | ||
|
|
b9cef10ef2 | ||
|
|
0dc41592f2 | ||
|
|
aabf3c54ea | ||
|
|
c2a57fd72b | ||
|
|
52386e08f2 | ||
|
|
b724d367fc | ||
|
|
07924751ad | ||
|
|
defa8df7ca | ||
|
|
bf8580fa88 | ||
|
|
c7b1e4dfa1 | ||
|
|
139a7c8249 | ||
|
|
158da51a5d | ||
|
|
f9ad99e572 | ||
|
|
1c65f5c150 | ||
|
|
0fb75b715d | ||
|
|
9364055283 | ||
|
|
1929b23ea8 | ||
|
|
efc7be0b6b | ||
|
|
327159c2ae | ||
|
|
af801df421 | ||
|
|
79c1aa60a2 | ||
|
|
a9535d3fd4 | ||
|
|
0ae6506bc1 | ||
|
|
a8c03262c2 | ||
|
|
687669ce17 | ||
|
|
bf6dd665f0 | ||
|
|
9250b91bb0 | ||
|
|
b934c74c30 | ||
|
|
96810b5ba1 | ||
|
|
ee08118ed3 | ||
|
|
051353e7f1 | ||
|
|
19f52429b0 | ||
|
|
4ed15d2755 | ||
|
|
154af1367a | ||
|
|
1de60bcfeb | ||
|
|
3679854c43 | ||
|
|
ce64c494f4 | ||
|
|
df4aa24913 | ||
|
|
aa54c013f8 | ||
|
|
0014ca6865 | ||
|
|
6c00c9b2b8 | ||
|
|
46fc67f70d | ||
|
|
e91fe12219 | ||
|
|
2a8f7412dd | ||
|
|
64d634e406 | ||
|
|
39cfe18ffe | ||
|
|
eac45c5e23 | ||
|
|
446c2b0f0e | ||
|
|
92e5aae901 | ||
|
|
bf3db721e2 | ||
|
|
8d4561c866 | ||
|
|
6627a211d7 | ||
|
|
0763a64b30 | ||
|
|
d333a31221 | ||
|
|
55a8a0d2d3 | ||
|
|
3093216534 | ||
|
|
99a1c36ffc | ||
|
|
f4e48f5fc1 | ||
|
|
c352dc647e | ||
|
|
34bb069b4a | ||
|
|
39ea9c112d | ||
|
|
e32d4648af | ||
|
|
20b7633ab5 | ||
|
|
6f017a7972 | ||
|
|
cbd0e2d287 | ||
|
|
ff739bf9be | ||
|
|
ff3541910b | ||
|
|
d043de7724 | ||
|
|
be1517facd | ||
|
|
19eced00f5 | ||
|
|
e2414b174a | ||
|
|
5e3c0cdea1 | ||
|
|
6b6d3d90e4 | ||
|
|
9b348adb6a | ||
|
|
feda420f0c | ||
|
|
4b818de0c8 | ||
|
|
614741d207 | ||
|
|
2550485c49 | ||
|
|
efc3e864bb | ||
|
|
1bf56fbe93 | ||
|
|
629629cb9e | ||
|
|
8c0751b186 | ||
|
|
0b46f61a23 | ||
|
|
0c92fbe11d | ||
|
|
59fa713549 | ||
|
|
63416d4f33 | ||
|
|
9e927af813 | ||
|
|
4b8bdeba74 | ||
|
|
4e4d67c8c0 | ||
|
|
ffda234b3c | ||
|
|
0bc44c36db | ||
|
|
f18bee5c3d | ||
|
|
73e16215bd | ||
|
|
75d7d2f172 | ||
|
|
269dca5b6d | ||
|
|
0fd6bed813 | ||
|
|
33bef6acd0 | ||
|
|
a6c4b31845 | ||
|
|
ea8ba8defc | ||
|
|
94cb78491a | ||
|
|
d12a5c5883 | ||
|
|
7a014761dc | ||
|
|
91ac952a72 | ||
|
|
225055fb0c | ||
|
|
d46e80c2ac | ||
|
|
fe364bd2da | ||
|
|
2756dff735 | ||
|
|
401f64b986 | ||
|
|
a02dcf485f | ||
|
|
f2ecd9818a | ||
|
|
bc81f659aa | ||
|
|
f8f038c5e9 | ||
|
|
fefb6acc57 | ||
|
|
461120ad84 | ||
|
|
bab72a5d2e | ||
|
|
e4011b4a23 | ||
|
|
899b59620d | ||
|
|
c75b5bec07 | ||
|
|
f0c10ba5ec | ||
|
|
7f4b95123f | ||
|
|
6238c625ae | ||
|
|
dbe387890f | ||
|
|
25517ad674 | ||
|
|
3047b5319b | ||
|
|
a5fa2ef8d0 | ||
|
|
59faffe0d5 | ||
|
|
b076a7dabc | ||
|
|
cbab279c3b | ||
|
|
61be5c9bc1 | ||
|
|
0a4f7da2df | ||
|
|
a87d82fd47 | ||
|
|
208d040f6f | ||
|
|
8ebcfbb8d1 | ||
|
|
20803451de | ||
|
|
2c5cb6abe4 | ||
|
|
522fd64f69 | ||
|
|
aaa6616777 | ||
|
|
7bc3591080 | ||
|
|
db059d84c3 | ||
|
|
e3a9e61046 | ||
|
|
03103e1efd | ||
|
|
08b72e2b90 | ||
|
|
ed8a14fdd7 | ||
|
|
0e50f6d9f8 | ||
|
|
833fe7c7b7 | ||
|
|
47bb3000f6 | ||
|
|
41dd7bbdde | ||
|
|
dc9a4f0dba | ||
|
|
891a656449 | ||
|
|
916ee66be2 | ||
|
|
9d7d838131 | ||
|
|
df29376b0e | ||
|
|
d72a1ee328 | ||
|
|
ce532e0ff6 | ||
|
|
f824490873 | ||
|
|
ebc70feef0 | ||
|
|
226ac889c7 |
No files matched your search
@@ -37,6 +37,7 @@ BASECAMP3_USER_AGENT=
|
|||||||
|
|
||||||
DAILY_API_KEY=
|
DAILY_API_KEY=
|
||||||
DAILY_SCALE_PLAN=''
|
DAILY_SCALE_PLAN=''
|
||||||
|
DAILY_WEBHOOK_SECRET=''
|
||||||
|
|
||||||
# - GOOGLE CALENDAR/MEET/LOGIN
|
# - GOOGLE CALENDAR/MEET/LOGIN
|
||||||
# Needed to enable Google Calendar integration and Login with Google
|
# Needed to enable Google Calendar integration and Login with Google
|
||||||
@@ -126,4 +127,12 @@ ZOHOCRM_CLIENT_ID=""
|
|||||||
ZOHOCRM_CLIENT_SECRET=""
|
ZOHOCRM_CLIENT_SECRET=""
|
||||||
|
|
||||||
|
|
||||||
|
# - REVERT
|
||||||
|
# Used for the Pipedrive integration (via/ Revert (https://revert.dev))
|
||||||
|
# @see https://github.com/calcom/cal.com/#obtaining-revert-api-keys
|
||||||
|
REVERT_API_KEY=
|
||||||
|
REVERT_PUBLIC_TOKEN=
|
||||||
|
|
||||||
|
# NOTE: If you're self hosting Revert, update this URL to point to your own instance.
|
||||||
|
REVERT_API_URL=https://api.revert.dev/
|
||||||
# *********************************************************************************************************
|
# *********************************************************************************************************
|
||||||
+77
-3
@@ -25,6 +25,7 @@ CALCOM_LICENSE_KEY=
|
|||||||
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
||||||
UPSTASH_REDIS_REST_URL=
|
UPSTASH_REDIS_REST_URL=
|
||||||
UPSTASH_REDIS_REST_TOKEN=
|
UPSTASH_REDIS_REST_TOKEN=
|
||||||
|
INSIGHTS_DATABASE_URL=
|
||||||
|
|
||||||
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
|
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
|
||||||
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
||||||
@@ -87,7 +88,7 @@ CRON_ENABLE_APP_SYNC=false
|
|||||||
|
|
||||||
# Application Key for symmetric encryption and decryption
|
# Application Key for symmetric encryption and decryption
|
||||||
# must be 32 bytes for AES256 encryption algorithm
|
# must be 32 bytes for AES256 encryption algorithm
|
||||||
# You can use: `openssl rand -base64 24` to generate one
|
# You can use: `openssl rand -base64 32` to generate one
|
||||||
CALENDSO_ENCRYPTION_KEY=
|
CALENDSO_ENCRYPTION_KEY=
|
||||||
|
|
||||||
# Intercom Config
|
# Intercom Config
|
||||||
@@ -106,6 +107,19 @@ NEXT_PUBLIC_HELPSCOUT_KEY=
|
|||||||
NEXT_PUBLIC_FRESHCHAT_TOKEN=
|
NEXT_PUBLIC_FRESHCHAT_TOKEN=
|
||||||
NEXT_PUBLIC_FRESHCHAT_HOST=
|
NEXT_PUBLIC_FRESHCHAT_HOST=
|
||||||
|
|
||||||
|
# Google OAuth credentials
|
||||||
|
# To enable Login with Google you need to:
|
||||||
|
# 1. Set `GOOGLE_API_CREDENTIALS` above
|
||||||
|
# 2. Set `GOOGLE_LOGIN_ENABLED` to `true`
|
||||||
|
# When self-hosting please ensure you configure the Google integration as an Internal app so no one else can login to your instance
|
||||||
|
# @see https://support.google.com/cloud/answer/6158849#public-and-internal&zippy=%2Cpublic-and-internal-applications
|
||||||
|
GOOGLE_LOGIN_ENABLED=false
|
||||||
|
|
||||||
|
# - GOOGLE CALENDAR/MEET/LOGIN
|
||||||
|
# Needed to enable Google Calendar integration and Login with Google
|
||||||
|
# @see https://github.com/calcom/cal.com#obtaining-the-google-api-credentials
|
||||||
|
GOOGLE_API_CREDENTIALS=
|
||||||
|
|
||||||
# Inbox to send user feedback
|
# Inbox to send user feedback
|
||||||
SEND_FEEDBACK_EMAIL=
|
SEND_FEEDBACK_EMAIL=
|
||||||
|
|
||||||
@@ -115,6 +129,15 @@ SENDGRID_API_KEY=
|
|||||||
SENDGRID_EMAIL=
|
SENDGRID_EMAIL=
|
||||||
NEXT_PUBLIC_SENDGRID_SENDER_NAME=
|
NEXT_PUBLIC_SENDGRID_SENDER_NAME=
|
||||||
|
|
||||||
|
# Sentry
|
||||||
|
# Used for capturing exceptions and logging messages
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN=
|
||||||
|
|
||||||
|
# Formbricks Experience Management Integration
|
||||||
|
FORMBRICKS_HOST_URL=https://app.formbricks.com
|
||||||
|
FORMBRICKS_ENVIRONMENT_ID=
|
||||||
|
FORMBRICKS_FEEDBACK_SURVEY_ID=
|
||||||
|
|
||||||
# Twilio
|
# Twilio
|
||||||
# Used to send SMS reminders in workflows
|
# Used to send SMS reminders in workflows
|
||||||
TWILIO_SID=
|
TWILIO_SID=
|
||||||
@@ -126,7 +149,7 @@ TWILIO_WHATSAPP_PHONE_NUMBER=
|
|||||||
NEXT_PUBLIC_SENDER_ID=
|
NEXT_PUBLIC_SENDER_ID=
|
||||||
TWILIO_VERIFY_SID=
|
TWILIO_VERIFY_SID=
|
||||||
|
|
||||||
# Set it to "1" if you need to run E2E tests locally.
|
# Set it to "1" if you need to run E2E tests locally.
|
||||||
NEXT_PUBLIC_IS_E2E=
|
NEXT_PUBLIC_IS_E2E=
|
||||||
|
|
||||||
# Used for internal billing system
|
# Used for internal billing system
|
||||||
@@ -137,6 +160,7 @@ NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
|
|||||||
STRIPE_TEAM_MONTHLY_PRICE_ID=
|
STRIPE_TEAM_MONTHLY_PRICE_ID=
|
||||||
STRIPE_ORG_MONTHLY_PRICE_ID=
|
STRIPE_ORG_MONTHLY_PRICE_ID=
|
||||||
STRIPE_WEBHOOK_SECRET=
|
STRIPE_WEBHOOK_SECRET=
|
||||||
|
STRIPE_WEBHOOK_SECRET_APPS=
|
||||||
STRIPE_PRIVATE_KEY=
|
STRIPE_PRIVATE_KEY=
|
||||||
STRIPE_CLIENT_ID=
|
STRIPE_CLIENT_ID=
|
||||||
PAYMENT_FEE_FIXED=
|
PAYMENT_FEE_FIXED=
|
||||||
@@ -177,6 +201,10 @@ EMAIL_SERVER_PORT=1025
|
|||||||
# Make sure to run mailhog container manually or with `yarn dx`
|
# Make sure to run mailhog container manually or with `yarn dx`
|
||||||
E2E_TEST_MAILHOG_ENABLED=
|
E2E_TEST_MAILHOG_ENABLED=
|
||||||
|
|
||||||
|
# Resend
|
||||||
|
# Send transactional email using resend
|
||||||
|
# RESEND_API_KEY=
|
||||||
|
|
||||||
# **********************************************************************************************************
|
# **********************************************************************************************************
|
||||||
|
|
||||||
# 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
|
||||||
@@ -203,6 +231,8 @@ CSP_POLICY=
|
|||||||
EDGE_CONFIG=
|
EDGE_CONFIG=
|
||||||
|
|
||||||
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
||||||
|
# Control time intervals on a user's Schedule availability
|
||||||
|
NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL=
|
||||||
|
|
||||||
# - ORGANIZATIONS *******************************************************************************************
|
# - ORGANIZATIONS *******************************************************************************************
|
||||||
# Enable Organizations non-prod domain setup, works in combination with organizations feature flag
|
# Enable Organizations non-prod domain setup, works in combination with organizations feature flag
|
||||||
@@ -224,12 +254,32 @@ PROJECT_ID_VERCEL=
|
|||||||
TEAM_ID_VERCEL=
|
TEAM_ID_VERCEL=
|
||||||
# Get it from: https://vercel.com/account/tokens
|
# Get it from: https://vercel.com/account/tokens
|
||||||
AUTH_BEARER_TOKEN_VERCEL=
|
AUTH_BEARER_TOKEN_VERCEL=
|
||||||
|
# Add the main domain that you want to use for testing vercel domain management for organizations. This is necessary because WEBAPP_URL of local isn't a valid public domain
|
||||||
|
# Would create org1.example.com for an org with slug org1
|
||||||
|
# LOCAL_TESTING_DOMAIN_VERCEL="example.com"
|
||||||
|
|
||||||
|
## Set it to 1 if you use cloudflare to manage your DNS and would like us to manage the DNS for you for organizations
|
||||||
|
# CLOUDFLARE_DNS=1
|
||||||
|
## Get it from: https://dash.cloudflare.com/profile/api-tokens. Select Edit Zone template and choose a zone(your domain)
|
||||||
|
# AUTH_BEARER_TOKEN_CLOUDFLARE=
|
||||||
|
## Zone ID can be found in the Overview tab of your domain in Cloudflare
|
||||||
|
# CLOUDFLARE_ZONE_ID=
|
||||||
|
## It should usually work with the default value. This is the DNS CNAME record content to point to Vercel domain
|
||||||
|
# CLOUDFLARE_VERCEL_CNAME=cname.vercel-dns.com
|
||||||
|
|
||||||
# - APPLE CALENDAR
|
# - APPLE CALENDAR
|
||||||
# Used for E2E tests on Apple Calendar
|
# Used for E2E tests on Apple Calendar
|
||||||
E2E_TEST_APPLE_CALENDAR_EMAIL=""
|
E2E_TEST_APPLE_CALENDAR_EMAIL=""
|
||||||
E2E_TEST_APPLE_CALENDAR_PASSWORD=""
|
E2E_TEST_APPLE_CALENDAR_PASSWORD=""
|
||||||
|
|
||||||
|
# - CALCOM QA ACCOUNT
|
||||||
|
# Used for E2E tests on Cal.com that require 3rd party integrations
|
||||||
|
E2E_TEST_CALCOM_QA_EMAIL="qa@example.com"
|
||||||
|
# Replace with your own password
|
||||||
|
E2E_TEST_CALCOM_QA_PASSWORD="password"
|
||||||
|
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS=
|
||||||
|
E2E_TEST_CALCOM_GCAL_KEYS=
|
||||||
|
|
||||||
# - APP CREDENTIAL SYNC ***********************************************************************************
|
# - APP CREDENTIAL SYNC ***********************************************************************************
|
||||||
# Used for self-hosters that are implementing Cal.com into their applications that already have certain integrations
|
# Used for self-hosters that are implementing Cal.com into their applications that already have certain integrations
|
||||||
# Under settings/admin/apps ensure that all app secrets are set the same as the parent application
|
# Under settings/admin/apps ensure that all app secrets are set the same as the parent application
|
||||||
@@ -239,7 +289,7 @@ CALCOM_WEBHOOK_SECRET=""
|
|||||||
CALCOM_WEBHOOK_HEADER_NAME="calcom-webhook-secret"
|
CALCOM_WEBHOOK_HEADER_NAME="calcom-webhook-secret"
|
||||||
CALCOM_CREDENTIAL_SYNC_ENDPOINT=""
|
CALCOM_CREDENTIAL_SYNC_ENDPOINT=""
|
||||||
# Key should match on Cal.com and your application
|
# Key should match on Cal.com and your application
|
||||||
# must be 32 bytes for AES256 encryption algorithm
|
# must be 24 bytes for AES256 encryption algorithm
|
||||||
# You can use: `openssl rand -base64 24` to generate one
|
# You can use: `openssl rand -base64 24` to generate one
|
||||||
CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY=""
|
CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY=""
|
||||||
|
|
||||||
@@ -257,3 +307,27 @@ E2E_TEST_OIDC_USER_EMAIL=
|
|||||||
E2E_TEST_OIDC_USER_PASSWORD=
|
E2E_TEST_OIDC_USER_PASSWORD=
|
||||||
|
|
||||||
# ***********************************************************************************************************
|
# ***********************************************************************************************************
|
||||||
|
|
||||||
|
# provide a value between 0 and 100 to ensure the percentage of traffic
|
||||||
|
# redirected from the legacy to the future pages
|
||||||
|
AB_TEST_BUCKET_PROBABILITY=50
|
||||||
|
# whether we redirect to the future/event-types from event-types or not
|
||||||
|
APP_ROUTER_EVENT_TYPES_ENABLED=0
|
||||||
|
APP_ROUTER_SETTINGS_ADMIN_ENABLED=0
|
||||||
|
APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED=0
|
||||||
|
APP_ROUTER_APPS_SLUG_ENABLED=0
|
||||||
|
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
|
||||||
|
# whether we redirect to the future/apps/categories from /apps/categories or not
|
||||||
|
APP_ROUTER_APPS_CATEGORIES_ENABLED=0
|
||||||
|
# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not
|
||||||
|
APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0
|
||||||
|
APP_ROUTER_BOOKINGS_STATUS_ENABLED=0
|
||||||
|
APP_ROUTER_WORKFLOWS_ENABLED=0
|
||||||
|
APP_ROUTER_SETTINGS_TEAMS_ENABLED=0
|
||||||
|
APP_ROUTER_GETTING_STARTED_STEP_ENABLED=0
|
||||||
|
APP_ROUTER_APPS_ENABLED=0
|
||||||
|
APP_ROUTER_VIDEO_ENABLED=0
|
||||||
|
APP_ROUTER_TEAMS_ENABLED=0
|
||||||
|
|
||||||
|
# disable setry server source maps
|
||||||
|
SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
|
||||||
@@ -47,3 +47,9 @@ assignees: ""
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
(Share it here.)
|
(Share it here.)
|
||||||
|
|
||||||
|
---
|
||||||
|
##### House rules
|
||||||
|
- If this issue has a `🚨 needs approval` label, don't start coding yet. Wait until a core member approves feature request by removing this label, then you can start coding.
|
||||||
|
- For clarity: Non-core member issues automatically get the `🚨 needs approval` label.
|
||||||
|
- Your feature ideas are invaluable to us! However, they undergo review to ensure alignment with the product's direction.
|
||||||
@@ -19,11 +19,12 @@ Fixes # (issue)
|
|||||||
|
|
||||||
<!-- Please delete bullets that are not relevant. -->
|
<!-- Please delete bullets that are not relevant. -->
|
||||||
|
|
||||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
- Bug fix (non-breaking change which fixes an issue)
|
||||||
- [ ] Chore (refactoring code, technical debt, workflow improvements)
|
- Chore (refactoring code, technical debt, workflow improvements)
|
||||||
- [ ] New feature (non-breaking change which adds functionality)
|
- New feature (non-breaking change which adds functionality)
|
||||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
- [ ] This change requires a documentation update
|
- Tests (Unit/Integration/E2E or any other test)
|
||||||
|
- This change requires a documentation update
|
||||||
|
|
||||||
## How should this be tested?
|
## How should this be tested?
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ Fixes # (issue)
|
|||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
<!-- Please remove all the irrelevant bullets to your PR -->
|
<!-- Remove bullet points below that don't apply to you -->
|
||||||
|
|
||||||
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
|
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
|
||||||
- My code doesn't follow the style guidelines of this project
|
- My code doesn't follow the style guidelines of this project
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ runs:
|
|||||||
**/.turbo/**
|
**/.turbo/**
|
||||||
**/dist/**
|
**/dist/**
|
||||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||||
- run: yarn build
|
- run: |
|
||||||
|
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||||
|
yarn build
|
||||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -17,10 +17,14 @@ runs:
|
|||||||
cache-name: cache-db
|
cache-name: cache-db
|
||||||
key-1: ${{ hashFiles('packages/prisma/schema.prisma', 'packages/prisma/migrations/**/**.sql', 'packages/prisma/*.ts') }}
|
key-1: ${{ hashFiles('packages/prisma/schema.prisma', 'packages/prisma/migrations/**/**.sql', 'packages/prisma/*.ts') }}
|
||||||
key-2: ${{ github.event.pull_request.number || github.ref }}
|
key-2: ${{ github.event.pull_request.number || github.ref }}
|
||||||
|
DATABASE_URL: ${{ inputs.DATABASE_URL }}
|
||||||
|
E2E_TEST_CALCOM_QA_EMAIL: ${{ inputs.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||||
|
E2E_TEST_CALCOM_QA_PASSWORD: ${{ inputs.E2E_TEST_CALCOM_QA_PASSWORD }}
|
||||||
|
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ inputs.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
||||||
with:
|
with:
|
||||||
path: ${{ inputs.path }}
|
path: ${{ inputs.path }}
|
||||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }}
|
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }}
|
||||||
- run: yarn db-seed
|
- run: echo ${{ env.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }} && 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
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ runs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Cache playwright binaries
|
- name: Cache playwright binaries
|
||||||
id: playwright-cache
|
id: playwright-cache
|
||||||
uses: buildjet/cache@v2
|
uses: buildjet/cache@v3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/Library/Caches/ms-playwright
|
~/Library/Caches/ms-playwright
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
name: "Apply issue labels to PR"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
label_on_pr:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: none
|
|
||||||
issues: read
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Apply labels from linked issue to PR
|
|
||||||
uses: actions/github-script@v5
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
async function getLinkedIssues(owner, repo, prNumber) {
|
|
||||||
const query = `query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
|
|
||||||
repository(owner: $owner, name: $repo) {
|
|
||||||
pullRequest(number: $prNumber) {
|
|
||||||
closingIssuesReferences(first: 10) {
|
|
||||||
nodes {
|
|
||||||
number
|
|
||||||
labels(first: 10) {
|
|
||||||
nodes {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const variables = {
|
|
||||||
owner: owner,
|
|
||||||
repo: repo,
|
|
||||||
prNumber: prNumber,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await github.graphql(query, variables);
|
|
||||||
return result.repository.pullRequest.closingIssuesReferences.nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pr = context.payload.pull_request;
|
|
||||||
const linkedIssues = await getLinkedIssues(
|
|
||||||
context.repo.owner,
|
|
||||||
context.repo.repo,
|
|
||||||
pr.number
|
|
||||||
);
|
|
||||||
|
|
||||||
const labelsToAdd = new Set();
|
|
||||||
for (const issue of linkedIssues) {
|
|
||||||
if (issue.labels && issue.labels.nodes) {
|
|
||||||
for (const label of issue.labels.nodes) {
|
|
||||||
labelsToAdd.add(label.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (labelsToAdd.size) {
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: pr.number,
|
|
||||||
labels: Array.from(labelsToAdd),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ name: Check types
|
|||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: "--max-old-space-size=8192"
|
NODE_OPTIONS: --max-old-space-size=4096
|
||||||
jobs:
|
jobs:
|
||||||
check-types:
|
check-types:
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v7
|
- uses: actions/stale@v7
|
||||||
with:
|
with:
|
||||||
|
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: 14
|
||||||
days-before-pr-close: 7
|
days-before-pr-close: -1
|
||||||
stale-pr-message: "This PR is being marked as stale due to inactivity."
|
stale-pr-message: "This PR is being marked as stale due to inactivity."
|
||||||
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 is being closed due to inactivity. Please reopen if work is intended to be continued."
|
||||||
operations-per-run: 100
|
operations-per-run: 100
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
name: E2E App-Store Apps
|
name: E2E App-Store Apps Tests
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --max-old-space-size=4096
|
||||||
jobs:
|
jobs:
|
||||||
e2e-app-store:
|
e2e-app-store:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
@@ -29,10 +30,15 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/yarn-playwright-install
|
- uses: ./.github/actions/yarn-playwright-install
|
||||||
- uses: ./.github/actions/cache-db
|
- uses: ./.github/actions/cache-db
|
||||||
|
env:
|
||||||
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
|
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||||
|
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
|
||||||
|
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
||||||
|
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
||||||
- uses: ./.github/actions/cache-build
|
- uses: ./.github/actions/cache-build
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: yarn e2e:app-store --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
run: yarn e2e:app-store --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||||
@@ -43,6 +49,10 @@ jobs:
|
|||||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||||
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_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||||
|
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
|
||||||
|
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
||||||
|
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
||||||
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
|
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
|
||||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||||
@@ -65,7 +75,7 @@ jobs:
|
|||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: app-store-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
name: app-store-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
||||||
path: test-results
|
path: test-results
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
name: E2E Embed React tests and booking flow(for non-embed as well)
|
name: E2E Embed React tests and booking flow (for non-embed as well)
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --max-old-space-size=4096
|
||||||
jobs:
|
jobs:
|
||||||
e2e-embed:
|
e2e-embed:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
@@ -24,7 +25,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/yarn-playwright-install
|
- uses: ./.github/actions/yarn-playwright-install
|
||||||
- uses: ./.github/actions/cache-db
|
- uses: ./.github/actions/cache-db
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: embed-react-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
name: embed-react-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
||||||
path: test-results
|
path: test-results
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
name: E2E Embed Core tests and booking flow(for non-embed as well)
|
name: E2E Embed Core tests and booking flow (for non-embed as well)
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --max-old-space-size=4096
|
||||||
jobs:
|
jobs:
|
||||||
e2e-embed:
|
e2e-embed:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
@@ -29,7 +30,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/yarn-playwright-install
|
- uses: ./.github/actions/yarn-playwright-install
|
||||||
- uses: ./.github/actions/cache-db
|
- uses: ./.github/actions/cache-db
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: embed-core-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
name: embed-core-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
||||||
path: test-results
|
path: test-results
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
name: E2E test
|
name: E2E tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --max-old-space-size=4096
|
||||||
jobs:
|
jobs:
|
||||||
e2e:
|
e2e:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
@@ -28,7 +28,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/yarn-playwright-install
|
- uses: ./.github/actions/yarn-playwright-install
|
||||||
- uses: ./.github/actions/cache-db
|
- uses: ./.github/actions/cache-db
|
||||||
@@ -68,7 +67,7 @@ jobs:
|
|||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: test-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
name: test-results-${{ matrix.shard }}_${{ strategy.job-total }}
|
||||||
path: test-results
|
path: test-results
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
name: "Pull Request Labeler"
|
name: "Pull Request Labeler"
|
||||||
on:
|
on:
|
||||||
- pull_request_target
|
pull_request_target:
|
||||||
|
workflow_dispatch:
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
@@ -16,3 +17,81 @@ jobs:
|
|||||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
|
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
|
||||||
sync-labels: ""
|
sync-labels: ""
|
||||||
|
team-labels:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: equitybee/team-label-action@main
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
||||||
|
organization-name: calcom
|
||||||
|
ignore-labels: "admin, app-store, ai, authentication, automated-testing, devops, platform, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
|
||||||
|
apply-labels-from-issue:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: none
|
||||||
|
issues: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Apply labels from linked issue to PR
|
||||||
|
uses: actions/github-script@v5
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
async function getLinkedIssues(owner, repo, prNumber) {
|
||||||
|
const query = `query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
|
||||||
|
repository(owner: $owner, name: $repo) {
|
||||||
|
pullRequest(number: $prNumber) {
|
||||||
|
closingIssuesReferences(first: 10) {
|
||||||
|
nodes {
|
||||||
|
number
|
||||||
|
labels(first: 10) {
|
||||||
|
nodes {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
owner: owner,
|
||||||
|
repo: repo,
|
||||||
|
prNumber: prNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await github.graphql(query, variables);
|
||||||
|
return result.repository.pullRequest.closingIssuesReferences.nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pr = context.payload.pull_request;
|
||||||
|
const linkedIssues = await getLinkedIssues(
|
||||||
|
context.repo.owner,
|
||||||
|
context.repo.repo,
|
||||||
|
pr.number
|
||||||
|
);
|
||||||
|
|
||||||
|
const labelsToAdd = new Set();
|
||||||
|
for (const issue of linkedIssues) {
|
||||||
|
if (issue.labels && issue.labels.nodes) {
|
||||||
|
for (const label of issue.labels.nodes) {
|
||||||
|
labelsToAdd.add(label.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelsToAdd.size) {
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: pr.number,
|
||||||
|
labels: Array.from(labelsToAdd),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload ESLint report
|
- name: Upload ESLint report
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: lint-results
|
name: lint-results
|
||||||
path: lint-results
|
path: lint-results
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
name: Auto Comment Merge Conflicts
|
|
||||||
on: push
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
auto-comment-merge-conflicts:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: codytseng/auto-comment-merge-conflicts@v1
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
comment-body: "Hey there, there is a merge conflict, can you take a look?"
|
|
||||||
wait-ms: 3000
|
|
||||||
max-retries: 5
|
|
||||||
label-name: "🚨 merge conflict"
|
|
||||||
ignore-authors: dependabot,otherAuthor
|
|
||||||
@@ -2,6 +2,7 @@ name: "Next.js Bundle Analysis"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -27,14 +28,14 @@ jobs:
|
|||||||
npx -p nextjs-bundle-analysis@0.5.0 report
|
npx -p nextjs-bundle-analysis@0.5.0 report
|
||||||
|
|
||||||
- name: Upload bundle
|
- name: Upload bundle
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: bundle
|
name: bundle
|
||||||
path: apps/web/.next/analyze/__bundle_analysis.json
|
path: apps/web/.next/analyze/__bundle_analysis.json
|
||||||
|
|
||||||
- name: Download base branch bundle stats
|
- name: Download base branch bundle stats
|
||||||
uses: dawidd6/action-download-artifact@v2
|
uses: dawidd6/action-download-artifact@v2
|
||||||
if: success() && github.event.number
|
if: success()
|
||||||
with:
|
with:
|
||||||
workflow: nextjs-bundle-analysis.yml
|
workflow: nextjs-bundle-analysis.yml
|
||||||
branch: ${{ github.event.pull_request.base.ref }}
|
branch: ${{ github.event.pull_request.base.ref }}
|
||||||
@@ -54,7 +55,7 @@ jobs:
|
|||||||
# Either of these arguments can be changed or removed by editing the `nextBundleAnalysis`
|
# Either of these arguments can be changed or removed by editing the `nextBundleAnalysis`
|
||||||
# entry in your package.json file.
|
# entry in your package.json file.
|
||||||
- name: Compare with base branch bundle
|
- name: Compare with base branch bundle
|
||||||
if: success() && github.event.number
|
if: success()
|
||||||
run: |
|
run: |
|
||||||
cd apps/web
|
cd apps/web
|
||||||
ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare
|
ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare
|
||||||
@@ -68,10 +69,10 @@ jobs:
|
|||||||
body="${body//'%'/'%25'}"
|
body="${body//'%'/'%25'}"
|
||||||
body="${body//$'\n'/'%0A'}"
|
body="${body//$'\n'/'%0A'}"
|
||||||
body="${body//$'\r'/'%0D'}"
|
body="${body//$'\r'/'%0D'}"
|
||||||
echo ::set-output name=body::$body
|
echo "{body}=${body}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Find Comment
|
- name: Find Comment
|
||||||
uses: peter-evans/find-comment@v1
|
uses: peter-evans/find-comment@v2
|
||||||
if: success() && github.event.number
|
if: success() && github.event.number
|
||||||
id: fc
|
id: fc
|
||||||
with:
|
with:
|
||||||
@@ -79,14 +80,14 @@ jobs:
|
|||||||
body-includes: "<!-- __NEXTJS_BUNDLE_@calcom/web -->"
|
body-includes: "<!-- __NEXTJS_BUNDLE_@calcom/web -->"
|
||||||
|
|
||||||
- name: Create Comment
|
- name: Create Comment
|
||||||
uses: peter-evans/create-or-update-comment@v1.4.4
|
uses: peter-evans/create-or-update-comment@v3
|
||||||
if: success() && github.event.number && steps.fc.outputs.comment-id == 0
|
if: success() && github.event.number && steps.fc.outputs.comment-id == 0
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.number }}
|
issue-number: ${{ github.event.number }}
|
||||||
body: ${{ steps.get-comment-body.outputs.body }}
|
body: ${{ steps.get-comment-body.outputs.body }}
|
||||||
|
|
||||||
- name: Update Comment
|
- name: Update Comment
|
||||||
uses: peter-evans/create-or-update-comment@v1.4.4
|
uses: peter-evans/create-or-update-comment@v3
|
||||||
if: success() && github.event.number && steps.fc.outputs.comment-id != 0
|
if: success() && github.event.number && steps.fc.outputs.comment-id != 0
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.number }}
|
issue-number: ${{ github.event.number }}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
name: Assign PR team labels
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
team-labels:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: equitybee/team-label-action@main
|
|
||||||
with:
|
|
||||||
repo-token: ${{ secrets.GH_ACCESS_TOKEN }}
|
|
||||||
organization-name: calcom
|
|
||||||
ignore-labels: "app-store, authentication, automated-testing, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
|
|
||||||
+31
-23
@@ -4,9 +4,6 @@ on:
|
|||||||
pull_request_target:
|
pull_request_target:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths-ignore:
|
|
||||||
- "**.md"
|
|
||||||
- ".github/CODEOWNERS"
|
|
||||||
merge_group:
|
merge_group:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -15,15 +12,21 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
login:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
changes:
|
changes:
|
||||||
name: Detect changes
|
name: Detect changes
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
outputs:
|
outputs:
|
||||||
app-store: ${{ steps.filter.outputs.app-store }}
|
has-files-requiring-all-checks: ${{ steps.filter.outputs.has-files-requiring-all-checks }}
|
||||||
embed: ${{ steps.filter.outputs.embed }}
|
|
||||||
embed-react: ${{ steps.filter.outputs.embed-react }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
@@ -31,78 +34,83 @@ jobs:
|
|||||||
id: filter
|
id: filter
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
app-store:
|
has-files-requiring-all-checks:
|
||||||
- 'apps/web/**'
|
- "!(**.md|.github/CODEOWNERS)"
|
||||||
- 'packages/app-store/**'
|
|
||||||
- 'playwright.config.ts'
|
|
||||||
embed:
|
|
||||||
- 'apps/web/**'
|
|
||||||
- 'packages/embeds/**'
|
|
||||||
- 'playwright.config.ts'
|
|
||||||
embed-react:
|
|
||||||
- 'apps/web/**'
|
|
||||||
- 'packages/embeds/**'
|
|
||||||
- 'playwright.config.ts'
|
|
||||||
|
|
||||||
type-check:
|
type-check:
|
||||||
name: Type check
|
name: Type check
|
||||||
|
needs: [changes]
|
||||||
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/check-types.yml
|
uses: ./.github/workflows/check-types.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
test:
|
test:
|
||||||
name: Unit tests
|
name: Unit tests
|
||||||
uses: ./.github/workflows/test.yml
|
needs: [changes]
|
||||||
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
|
uses: ./.github/workflows/unit-tests.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
name: Linters
|
name: Linters
|
||||||
|
needs: [changes]
|
||||||
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/lint.yml
|
uses: ./.github/workflows/lint.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Production build
|
name: Production build
|
||||||
|
needs: [changes]
|
||||||
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/production-build.yml
|
uses: ./.github/workflows/production-build.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
build-without-database:
|
build-without-database:
|
||||||
name: Production build (without database)
|
name: Production build (without database)
|
||||||
|
needs: [changes]
|
||||||
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/production-build-without-database.yml
|
uses: ./.github/workflows/production-build-without-database.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
name: E2E tests
|
name: E2E tests
|
||||||
needs: [changes, lint, build]
|
needs: [changes, lint, build]
|
||||||
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/e2e.yml
|
uses: ./.github/workflows/e2e.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
e2e-app-store:
|
e2e-app-store:
|
||||||
name: E2E App Store tests
|
name: E2E App Store tests
|
||||||
needs: [changes, lint, build]
|
needs: [changes, lint, build]
|
||||||
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/e2e-app-store.yml
|
uses: ./.github/workflows/e2e-app-store.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
e2e-embed:
|
e2e-embed:
|
||||||
name: E2E embeds tests
|
name: E2E embeds tests
|
||||||
needs: [changes, lint, build]
|
needs: [changes, lint, build]
|
||||||
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/e2e-embed.yml
|
uses: ./.github/workflows/e2e-embed.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
e2e-embed-react:
|
e2e-embed-react:
|
||||||
name: E2E React embeds tests
|
name: E2E React embeds tests
|
||||||
needs: [changes, lint, build]
|
needs: [changes, lint, build]
|
||||||
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/e2e-embed-react.yml
|
uses: ./.github/workflows/e2e-embed-react.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
analyze:
|
analyze:
|
||||||
needs: build
|
name: Analyze Build
|
||||||
|
needs: [changes, build]
|
||||||
|
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
required:
|
required:
|
||||||
needs: [lint, type-check, test, build, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
needs: [changes, lint, type-check, test, build, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: fail if conditional jobs failed
|
- name: fail if conditional jobs failed
|
||||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
if: needs.changes.outputs.has-files-requiring-all-checks == 'true' && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled'))
|
||||||
run: exit 1
|
run: exit 1
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
name: Pre-release checks
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
changes:
|
||||||
|
name: Detect changes
|
||||||
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
outputs:
|
||||||
|
app-store: ${{ steps.filter.outputs.app-store }}
|
||||||
|
embed: ${{ steps.filter.outputs.embed }}
|
||||||
|
embed-react: ${{ steps.filter.outputs.embed-react }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
- uses: dorny/paths-filter@v2
|
||||||
|
id: filter
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
app-store:
|
||||||
|
- 'apps/web/**'
|
||||||
|
- 'packages/app-store/**'
|
||||||
|
- 'playwright.config.ts'
|
||||||
|
embed:
|
||||||
|
- 'apps/web/**'
|
||||||
|
- 'packages/embeds/**'
|
||||||
|
- 'playwright.config.ts'
|
||||||
|
embed-react:
|
||||||
|
- 'apps/web/**'
|
||||||
|
- 'packages/embeds/**'
|
||||||
|
- 'playwright.config.ts'
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: Linters
|
||||||
|
uses: ./.github/workflows/lint.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Production build
|
||||||
|
uses: ./.github/workflows/production-build.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e:
|
||||||
|
name: E2E tests
|
||||||
|
needs: [changes, lint, build]
|
||||||
|
uses: ./.github/workflows/e2e.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e-app-store:
|
||||||
|
name: E2E App Store tests
|
||||||
|
needs: [changes, lint, build]
|
||||||
|
uses: ./.github/workflows/e2e-app-store.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e-embed:
|
||||||
|
name: E2E embeds tests
|
||||||
|
needs: [changes, lint, build]
|
||||||
|
uses: ./.github/workflows/e2e-embed.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
e2e-embed-react:
|
||||||
|
name: E2E React embeds tests
|
||||||
|
needs: [changes, lint, build]
|
||||||
|
uses: ./.github/workflows/e2e-embed-react.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
build-without-database:
|
||||||
|
name: Production build (without database)
|
||||||
|
uses: ./.github/workflows/production-build-without-database.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
required:
|
||||||
|
needs: [e2e, e2e-app-store, e2e-embed, e2e-embed-react, build-without-database]
|
||||||
|
if: always()
|
||||||
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
|
steps:
|
||||||
|
- name: fail if conditional jobs failed
|
||||||
|
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
||||||
|
run: exit 1
|
||||||
@@ -9,6 +9,10 @@ env:
|
|||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_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_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||||
|
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
|
||||||
|
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
||||||
|
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
||||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||||
|
|||||||
@@ -44,5 +44,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
header: pr-title-lint-error
|
header: pr-title-lint-error
|
||||||
message: |
|
message: |
|
||||||
Thank you for following the naming conventions! 🙏 Feel free to join our [discord](https://go.cal.com/discord) and post your PR link to [collect XP and win prizes!](https://cal.com/blog/community-incentives)
|
Thank you for following the naming conventions! 🙏 Feel free to join our [discord](https://go.cal.com/discord) and post your PR link.
|
||||||
|
|
||||||
@@ -11,7 +11,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=6144"' >> $GITHUB_ENV
|
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
# Should be an 8GB machine as per https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
|
# Should be an 8GB machine as per https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
|
||||||
- run: yarn test
|
- run: yarn test
|
||||||
|
# We could add different timezones here that we need to run our tests in
|
||||||
|
- run: TZ=America/Los_Angeles yarn test -- --timeZoneDependentTestsOnly
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
name: "Welcome new contributors"
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: opened
|
|
||||||
pull_request:
|
|
||||||
types: opened
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
welcome-message:
|
|
||||||
name: Welcoming New Users
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
if: github.event.action == 'opened'
|
|
||||||
steps:
|
|
||||||
- uses: actions/first-interaction@v1
|
|
||||||
with:
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
pr-message: |-
|
|
||||||
Thank you for making your first Pull Request and taking the time to improve Cal.com ! ❤️🎉
|
|
||||||
Feel free to join our [discord](https://go.cal.com/discord) and post your PR link to [collect XP and win prizes!](https://cal.com/blog/community-incentives)
|
|
||||||
issue-message: |
|
|
||||||
Thank you for opening your first issue, one of our team members will review it as soon as it possible. ❤️🎉
|
|
||||||
Vendored
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": true
|
"source.fixAll.eslint": "explicit"
|
||||||
},
|
},
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"spellright.language": ["en"],
|
"spellright.language": ["en"],
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
diff --git a/index.cjs b/index.cjs
|
||||||
|
index c83f700ae9998cd87b4c2d66ecbb2ad3d7b4603c..76a2200b57f0b9243e2c61464d578b67746ad5a4 100644
|
||||||
|
--- a/index.cjs
|
||||||
|
+++ b/index.cjs
|
||||||
|
@@ -13,8 +13,8 @@ function withMetadataArgument(func, _arguments) {
|
||||||
|
// https://github.com/babel/babel/issues/2212#issuecomment-131827986
|
||||||
|
// An alternative approach:
|
||||||
|
// https://www.npmjs.com/package/babel-plugin-add-module-exports
|
||||||
|
-exports = module.exports = min.parsePhoneNumberFromString
|
||||||
|
-exports['default'] = min.parsePhoneNumberFromString
|
||||||
|
+// exports = module.exports = min.parsePhoneNumberFromString
|
||||||
|
+// exports['default'] = min.parsePhoneNumberFromString
|
||||||
|
|
||||||
|
// `parsePhoneNumberFromString()` named export is now considered legacy:
|
||||||
|
// it has been promoted to a default export due to being too verbose.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
diff --git a/dist/commonjs/serverSideTranslations.js b/dist/commonjs/serverSideTranslations.js
|
||||||
|
index bcad3d02fbdfab8dacb1d85efd79e98623a0c257..fff668f598154a13c4030d1b4a90d5d9c18214ad 100644
|
||||||
|
--- a/dist/commonjs/serverSideTranslations.js
|
||||||
|
+++ b/dist/commonjs/serverSideTranslations.js
|
||||||
|
@@ -36,7 +36,6 @@ var _fs = _interopRequireDefault(require("fs"));
|
||||||
|
var _path = _interopRequireDefault(require("path"));
|
||||||
|
var _createConfig = require("./config/createConfig");
|
||||||
|
var _node = _interopRequireDefault(require("./createClient/node"));
|
||||||
|
-var _appWithTranslation = require("./appWithTranslation");
|
||||||
|
var _utils = require("./utils");
|
||||||
|
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
|
||||||
|
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2["default"])(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
|
||||||
|
@@ -110,12 +109,8 @@ var serverSideTranslations = /*#__PURE__*/function () {
|
||||||
|
lng: initialLocale
|
||||||
|
}));
|
||||||
|
localeExtension = config.localeExtension, localePath = config.localePath, fallbackLng = config.fallbackLng, reloadOnPrerender = config.reloadOnPrerender;
|
||||||
|
- if (!reloadOnPrerender) {
|
||||||
|
- _context.next = 18;
|
||||||
|
- break;
|
||||||
|
- }
|
||||||
|
_context.next = 18;
|
||||||
|
- return _appWithTranslation.globalI18n === null || _appWithTranslation.globalI18n === void 0 ? void 0 : _appWithTranslation.globalI18n.reloadResources();
|
||||||
|
+ return void 0;
|
||||||
|
case 18:
|
||||||
|
_createClient = (0, _node["default"])(_objectSpread(_objectSpread({}, config), {}, {
|
||||||
|
lng: initialLocale
|
||||||
+12
-1
@@ -2,7 +2,18 @@
|
|||||||
|
|
||||||
Contributions are what makes the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
Contributions are what makes the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
||||||
|
|
||||||
- Before jumping into a PR be sure to search [existing PRs](https://github.com/calcom/cal.com/pulls) or [issues](https://github.com/calcom/cal.com/issues) for an open or closed item that relates to your submission.
|
## House rules
|
||||||
|
|
||||||
|
- Before submitting a new issue or PR, check if it already exists in [issues](https://github.com/calcom/cal.com/issues) or [PRs](https://github.com/calcom/cal.com/pulls).
|
||||||
|
- GitHub issues: take note of the `🚨 needs approval` label.
|
||||||
|
- **For Contributors**:
|
||||||
|
- Feature Requests: Wait for a core member to approve and remove the `🚨 needs approval` label before you start coding or submit a PR.
|
||||||
|
- Bugs, Security, Performance, Documentation, etc.: You can start coding immediately, even if the `🚨 needs approval` label is present. This label mainly concerns feature requests.
|
||||||
|
- **Our Process**:
|
||||||
|
- Issues from non-core members automatically receive the `🚨 needs approval` label.
|
||||||
|
- We greatly value new feature ideas. To ensure consistency in the product's direction, they undergo review and approval.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Priorities
|
## Priorities
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<h3 align="center">Cal.com (formerly Calendso)</h3>
|
<h3 align="center">Cal.com (formerly Calendso)</h3>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
The open-source Calendly alternative.
|
The open-source Calendly successor.
|
||||||
<br />
|
<br />
|
||||||
<a href="https://cal.com"><strong>Learn more »</strong></a>
|
<a href="https://cal.com"><strong>Learn more »</strong></a>
|
||||||
<br />
|
<br />
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
# Scheduling infrastructure for absolutely everyone
|
# Scheduling infrastructure for absolutely everyone
|
||||||
|
|
||||||
The open source Calendly alternative. You are in charge
|
The open source Calendly successor. You are in charge
|
||||||
of your own data, workflow, and appearance.
|
of your own data, workflow, and appearance.
|
||||||
|
|
||||||
Calendly and other scheduling tools are awesome. It made our lives massively easier. We're using it for business meetings, seminars, yoga classes, and even calls with our families. However, most tools are very limited in terms of control and customization.
|
Calendly and other scheduling tools are awesome. It made our lives massively easier. We're using it for business meetings, seminars, yoga classes, and even calls with our families. However, most tools are very limited in terms of control and customization.
|
||||||
@@ -122,7 +122,7 @@ Here is what you need to be able to run Cal.com.
|
|||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
1. Clone the repo into a public GitHub repository (or fork https://github.com/calcom/cal.com/fork). If you plan to distribute the code, keep the source code public to comply with [AGPLv3](https://github.com/calcom/cal.com/blob/main/LICENSE). To clone in a private repository, [acquire a commercial license](https://cal.com/sales))
|
1. Clone the repo into a public GitHub repository (or fork https://github.com/calcom/cal.com/fork). If you plan to distribute the code, keep the source code public to comply with [AGPLv3](https://github.com/calcom/cal.com/blob/main/LICENSE). To clone in a private repository, [acquire a commercial license](https://cal.com/sales)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/calcom/cal.com.git
|
git clone https://github.com/calcom/cal.com.git
|
||||||
@@ -147,7 +147,7 @@ Here is what you need to be able to run Cal.com.
|
|||||||
|
|
||||||
- Duplicate `.env.example` to `.env`
|
- Duplicate `.env.example` to `.env`
|
||||||
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
|
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
|
||||||
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
- Use `openssl rand -base64 32` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
||||||
|
|
||||||
5. Setup Node
|
5. Setup Node
|
||||||
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
|
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
|
||||||
@@ -216,12 +216,11 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
|||||||
|
|
||||||
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 or render.
|
||||||
|
|
||||||
- [Setup postgres DB with railway.app](https://arctype.com/postgres/setup/railway-postgres)
|
- [Setup postgres DB with railway.app](https://docs.railway.app/guides/postgresql)
|
||||||
- [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`.
|
||||||
|
|
||||||
1. Set a 32 character random string in your `.env` file for the `CALENDSO_ENCRYPTION_KEY` (You can use a command like `openssl rand -base64 24` to generate one).
|
|
||||||
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
|
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
|
||||||
|
|
||||||
In a development environment, run:
|
In a development environment, run:
|
||||||
@@ -381,6 +380,10 @@ Currently Vercel Pro Plan is required to be able to Deploy this application with
|
|||||||
|
|
||||||
[](https://render.com/deploy?repo=https://github.com/calcom/docker)
|
[](https://render.com/deploy?repo=https://github.com/calcom/docker)
|
||||||
|
|
||||||
|
### Elestio
|
||||||
|
|
||||||
|
[](https://elest.io/open-source/cal.com)
|
||||||
|
|
||||||
<!-- ROADMAP -->
|
<!-- ROADMAP -->
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
@@ -551,6 +554,10 @@ following
|
|||||||
|
|
||||||
[Follow these steps](./packages/app-store/zoho-bigin/)
|
[Follow these steps](./packages/app-store/zoho-bigin/)
|
||||||
|
|
||||||
|
### Obtaining Pipedrive Client ID and Secret
|
||||||
|
|
||||||
|
[Follow these steps](./packages/app-store/pipedrive-crm/)
|
||||||
|
|
||||||
## Workflows
|
## Workflows
|
||||||
|
|
||||||
### Setting up SendGrid for Email reminders
|
### Setting up SendGrid for Email reminders
|
||||||
@@ -593,8 +600,6 @@ Distributed under the [AGPLv3 License](https://github.com/calcom/cal.com/blob/ma
|
|||||||
|
|
||||||
Special thanks to these amazing projects which help power Cal.com:
|
Special thanks to these amazing projects which help power Cal.com:
|
||||||
|
|
||||||
[<img src="https://cal.com/powered-by-vercel.svg">](https://vercel.com/?utm_source=calend-so&utm_campaign=oss)
|
|
||||||
|
|
||||||
- [Vercel](https://vercel.com/?utm_source=calend-so&utm_campaign=oss)
|
- [Vercel](https://vercel.com/?utm_source=calend-so&utm_campaign=oss)
|
||||||
- [Next.js](https://nextjs.org/)
|
- [Next.js](https://nextjs.org/)
|
||||||
- [Day.js](https://day.js.org/)
|
- [Day.js](https://day.js.org/)
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Checkly Tests
|
||||||
|
|
||||||
|
Run as `yarn checkly test`
|
||||||
|
Deploy the tests as `yarn checkly deploy`
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import type { Page } from "@playwright/test";
|
||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("Org", () => {
|
||||||
|
// Because these pages involve next.config.js rewrites, it's better to test them on production
|
||||||
|
test.describe("Embeds - i.cal.com", () => {
|
||||||
|
test("Org Profile Page should be embeddable", async ({ page }) => {
|
||||||
|
const response = await page.goto("https://i.cal.com/embed");
|
||||||
|
expect(response?.status()).toBe(200);
|
||||||
|
await page.screenshot({ path: "screenshot.jpg" });
|
||||||
|
await expectPageToBeServerSideRendered(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Org User(Peer) Page should be embeddable", async ({ page }) => {
|
||||||
|
const response = await page.goto("https://i.cal.com/peer/embed");
|
||||||
|
expect(response?.status()).toBe(200);
|
||||||
|
await expect(page.locator("text=Peer Richelsen")).toBeVisible();
|
||||||
|
await expectPageToBeServerSideRendered(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Org User Event(peer/meet) Page should be embeddable", async ({ page }) => {
|
||||||
|
const response = await page.goto("https://i.cal.com/peer/meet/embed");
|
||||||
|
expect(response?.status()).toBe(200);
|
||||||
|
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
|
||||||
|
await expectPageToBeServerSideRendered(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Org Team Profile(/sales) page should be embeddable", async ({ page }) => {
|
||||||
|
const response = await page.goto("https://i.cal.com/sales/embed");
|
||||||
|
expect(response?.status()).toBe(200);
|
||||||
|
await expect(page.locator("text=Cal.com Sales")).toBeVisible();
|
||||||
|
await expectPageToBeServerSideRendered(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Org Team Event page(/sales/hippa) should be embeddable", async ({ page }) => {
|
||||||
|
const response = await page.goto("https://i.cal.com/sales/hipaa/embed");
|
||||||
|
expect(response?.status()).toBe(200);
|
||||||
|
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
|
||||||
|
await expectPageToBeServerSideRendered(page);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test.describe("Dynamic Group Booking", () => {
|
||||||
|
test("Dynamic Group booking link should load", async ({ page }) => {
|
||||||
|
const users = [
|
||||||
|
{
|
||||||
|
username: "peer",
|
||||||
|
name: "Peer Richelsen",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "bailey",
|
||||||
|
name: "Bailey Pumfleet",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const response = await page.goto(`http://i.cal.com/${users[0].username}+${users[1].username}`);
|
||||||
|
expect(response?.status()).toBe(200);
|
||||||
|
expect(await page.locator('[data-testid="event-title"]').textContent()).toBe("Dynamic");
|
||||||
|
|
||||||
|
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[0].name);
|
||||||
|
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[1].name);
|
||||||
|
// 2 users and 1 for the organization(2+1)
|
||||||
|
expect((await page.locator('[data-testid="event-meta"] [data-testid="avatar"]').all()).length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// This ensures that the route is actually mapped to a page that is using withEmbedSsr
|
||||||
|
async function expectPageToBeServerSideRendered(page: Page) {
|
||||||
|
expect(
|
||||||
|
await page.evaluate(() => {
|
||||||
|
return window.__NEXT_DATA__.props.pageProps.isEmbed;
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
}
|
||||||
@@ -83,6 +83,10 @@
|
|||||||
"NEXT_PUBLIC_TEAM_IMPERSONATION": {
|
"NEXT_PUBLIC_TEAM_IMPERSONATION": {
|
||||||
"description": "Set the following value to true if you wish to enable Team Impersonation",
|
"description": "Set the following value to true if you wish to enable Team Impersonation",
|
||||||
"value": "false"
|
"value": "false"
|
||||||
|
},
|
||||||
|
"NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL": {
|
||||||
|
"description": "Control time intervals on a user's Schedule availability",
|
||||||
|
"value": "15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ FRONTEND_URL=http://localhost:3000
|
|||||||
APP_ID=cal-ai
|
APP_ID=cal-ai
|
||||||
APP_URL=http://localhost:3000/apps/cal-ai
|
APP_URL=http://localhost:3000/apps/cal-ai
|
||||||
|
|
||||||
|
# This is for the onboard route. Which domain should we send emails from?
|
||||||
|
SENDER_DOMAIN=cal.ai
|
||||||
|
|
||||||
# Used to verify requests from sendgrid. You can generate a new one with: `openssl rand -hex 32`
|
# Used to verify requests from sendgrid. You can generate a new one with: `openssl rand -hex 32`
|
||||||
PARSE_KEY=
|
PARSE_KEY=
|
||||||
|
|
||||||
|
|||||||
+38
-18
@@ -1,12 +1,12 @@
|
|||||||
# Cal.com Email Assistant
|
# Cal.ai
|
||||||
|
|
||||||
Welcome to the first stage of Cal.ai!
|
Welcome to [Cal.ai](https://cal.ai)!
|
||||||
|
|
||||||
This app lets you chat with your calendar via email:
|
This app lets you chat with your calendar via email:
|
||||||
|
|
||||||
- Turn informal emails into bookings eg. forward "wanna meet tmrw at 2pm?"
|
- Turn informal emails into bookings eg. forward "wanna meet tmrw at 2pm?"
|
||||||
- List and rearrange your bookings eg. "Cancel my next meeting"
|
- List and rearrange your bookings eg. "clear my afternoon"
|
||||||
- Answer basic questions about your busiest times eg. "How does my Tuesday look?"
|
- Answer basic questions about your busiest times eg. "how does my Tuesday look?"
|
||||||
|
|
||||||
The core logic is contained in [agent/route.ts](/apps/ai/src/app/api/agent/route.ts). Here, a [LangChain Agent Executor](https://docs.langchain.com/docs/components/agents/agent-executor) is tasked with following your instructions. Given your last-known timezone, working hours, and busy times, it attempts to CRUD your bookings.
|
The core logic is contained in [agent/route.ts](/apps/ai/src/app/api/agent/route.ts). Here, a [LangChain Agent Executor](https://docs.langchain.com/docs/components/agents/agent-executor) is tasked with following your instructions. Given your last-known timezone, working hours, and busy times, it attempts to CRUD your bookings.
|
||||||
|
|
||||||
@@ -14,7 +14,11 @@ _The AI agent can only choose from a set of tools, without ever seeing your API
|
|||||||
|
|
||||||
Emails are cleaned and routed in [receive/route.ts](/apps/ai/src/app/api/receive/route.ts) using [MailParser](https://nodemailer.com/extras/mailparser/).
|
Emails are cleaned and routed in [receive/route.ts](/apps/ai/src/app/api/receive/route.ts) using [MailParser](https://nodemailer.com/extras/mailparser/).
|
||||||
|
|
||||||
Incoming emails are routed by email address. Addresses are verified by [DKIM record](https://support.google.com/a/answer/174124?hl=en), making it hard to spoof them.
|
Incoming emails are routed by email address. Addresses are verified by [DKIM record](https://support.google.com/a/answer/174124?hl=en), making them hard to spoof.
|
||||||
|
|
||||||
|
## Recognition
|
||||||
|
|
||||||
|
<a href="https://www.producthunt.com/posts/cal-ai?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-cal-ai" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=419860&theme=light&period=daily" alt="Cal.ai - World's first open source AI scheduling assistant | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/cal-ai?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cal-ai" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=419860&theme=light" alt="Cal.ai - World's first open source AI scheduling assistant | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
@@ -22,27 +26,43 @@ Incoming emails are routed by email address. Addresses are verified by [DKIM rec
|
|||||||
|
|
||||||
If you haven't yet, please run the [root setup](/README.md) steps.
|
If you haven't yet, please run the [root setup](/README.md) steps.
|
||||||
|
|
||||||
Before running the app, please see [env.mjs](./src/env.mjs) for all required environment variables. You'll need:
|
Before running the app, please see [env.mjs](./src/env.mjs) for all required environment variables. Run `cp .env.example .env` in this folder to get started. You'll need:
|
||||||
|
|
||||||
- An [OpenAI API key](https://platform.openai.com/account/api-keys) with access to GPT-4
|
- An [OpenAI API key](https://platform.openai.com/account/api-keys) with access to GPT-4
|
||||||
- A [SendGrid API key](https://app.sendgrid.com/settings/api_keys)
|
- A [SendGrid API key](https://app.sendgrid.com/settings/api_keys)
|
||||||
- A default sender email (for example, `ai@cal.dev`)
|
- A default sender email (for example, `me@dev.example.com`)
|
||||||
- The Cal.ai's app ID and URL (see [add.ts](/packages/app-store/cal-ai/api/index.ts))
|
- The Cal.ai app's ID and URL (see [add.ts](/packages/app-store/cal-ai/api/index.ts))
|
||||||
|
- A unique value for `PARSE_KEY` with `openssl rand -hex 32`
|
||||||
|
|
||||||
To stand up the API and AI apps simultaneously, simply run `yarn dev:ai`.
|
To stand up the API and AI apps simultaneously, simply run `yarn dev:ai`.
|
||||||
|
|
||||||
|
### Agent Architecture
|
||||||
|
|
||||||
|
The scheduling agent in [agent/route.ts](/apps/ai/src/app/api/agent/route.ts) calls an LLM (in this case, GPT-4) in a loop to accomplish a multi-step task. We use an [OpenAI Functions agent](https://js.langchain.com/docs/modules/agents/agent_types/openai_functions_agent), which is fine-tuned to output text suited for passing to tools.
|
||||||
|
|
||||||
|
Tools (eg. [`createBooking`](/apps/ai/src/tools/createBooking.ts)) are simply JavaScript methods wrapped by Zod schemas, telling the agent what format to output.
|
||||||
|
|
||||||
|
Here is the full architecture:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
### Email Router
|
### Email Router
|
||||||
|
|
||||||
To expose the AI app, run `ngrok http 3000` (or the AI app's port number) in a new terminal. You may need to install [nGrok](https://ngrok.com/).
|
To expose the AI app, you can use either [Tunnelmole](https://github.com/robbie-cahill/tunnelmole-client), an open source tunnelling tool; or [nGrok](https://ngrok.com/), a popular closed source tunnelling tool.
|
||||||
|
|
||||||
To forward incoming emails to the Node.js server, one option is to use [SendGrid's Inbound Parse Webhook](https://docs.sendgrid.com/for-developers/parsing-email/setting-up-the-inbound-parse-webhook).
|
For Tunnelmole, run `tmole 3005` (or the AI app's port number) in a new terminal. Please replace `3005` with the port number if it is different. In the output, you'll see two URLs, one http and a https (we recommend using the https url for privacy and security). To install Tunnelmole, use `curl -O https://install.tunnelmole.com/8dPBw/install && sudo bash install`. (On Windows, download [tmole.exe](https://tunnelmole.com/downloads/tmole.exe))
|
||||||
|
|
||||||
1. [Sign up for an account](https://signup.sendgrid.com/)
|
For nGrok, run `ngrok http 3005` (or the AI app's port number) in a new terminal. You may need to install nGrok first.
|
||||||
2. Go to Settings > [Inbound Parse](https://app.sendgrid.com/settings/parse) > Add Host & URL.
|
|
||||||
3. For subdomain, use `<sub>.<domain>.com` for now, where `sub` can be any subdomain but `domain.com` will need to be verified via MX records in your environment variables, eg. on [Vercel](https://vercel.com/guides/how-to-add-vercel-environment-variables).
|
|
||||||
4. Use the nGrok URL from above as the **Destination URL**.
|
|
||||||
5. Activate "POST the raw, full MIME message".
|
|
||||||
6. Send an email to `<anyone>@ai.example.com`. You should see a ping on the nGrok listener and Node.js server.
|
|
||||||
7. Adjust the logic in [receive/route.ts](/apps/ai/src/app/api/receive/route.ts), save to hot-reload, and send another email to test the behaviour.
|
|
||||||
|
|
||||||
Please feel free to improve any part of this architecture.
|
To forward incoming emails to the serverless function at `/agent`, we use [SendGrid's Inbound Parse](https://docs.sendgrid.com/for-developers/parsing-email/setting-up-the-inbound-parse-webhook).
|
||||||
|
|
||||||
|
1. Ensure you have a [SendGrid account](https://signup.sendgrid.com/)
|
||||||
|
2. Ensure you have an authenticated domain. Go to Settings > Sender Authentication > Authenticate. For DNS host, select `I'm not sure`. Click Next and add your domain, eg. `example.com`. Choose Manual Setup. You'll be given three CNAME records to add to your DNS settings, eg. in [Vercel Domains](https://vercel.com/dashboard/domains). After adding those records, click Verify. To troubleshoot, see the [full instructions](https://docs.sendgrid.com/ui/account-and-settings/how-to-set-up-domain-authentication).
|
||||||
|
3. Authorize your domain for email with MX records: one with name `[your domain].com` and value `mx.sendgrid.net.`, and another with name `bounces.[your domain].com` and value `feedback-smtp.us-east-1.amazonses.com`. Set the priority to `10` if prompted.
|
||||||
|
4. Go to Settings > [Inbound Parse](https://app.sendgrid.com/settings/parse) > Add Host & URL. Choose your authenticated domain.
|
||||||
|
5. In the Destination URL field, use the Tunnelmole or ngrok URL from above along with the path, `/api/receive`, and one param, `parseKey`, which lives in [this app's .env](/apps/ai/.env.example) under `PARSE_KEY`. The full URL should look like `https://abc.tunnelmole.net/api/receive?parseKey=ABC-123` or `https://abc.ngrok.io/api/receive?parseKey=ABC-123`.
|
||||||
|
6. Activate "POST the raw, full MIME message".
|
||||||
|
7. Send an email to `[anyUsername]@example.com`. You should see a ping on the Tunnelmole or ngrok listener and server.
|
||||||
|
8. Adjust the logic in [receive/route.ts](/apps/ai/src/app/api/receive/route.ts), save to hot-reload, and send another email to test the behaviour.
|
||||||
|
|
||||||
|
Please feel free to improve any part of this architecture!
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/ai",
|
"name": "@calcom/ai",
|
||||||
"version": "1.1.1",
|
"version": "1.2.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"author": "Cal.com Inc.",
|
"author": "Cal.com Inc.",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
"@t3-oss/env-nextjs": "^0.6.1",
|
"@t3-oss/env-nextjs": "^0.6.1",
|
||||||
"langchain": "^0.0.131",
|
"langchain": "^0.0.131",
|
||||||
"mailparser": "^3.6.5",
|
"mailparser": "^3.6.5",
|
||||||
"next": "^13.4.6",
|
"next": "^13.5.4",
|
||||||
"supports-color": "8.1.1",
|
"supports-color": "8.1.1",
|
||||||
"zod": "^3.22.2"
|
"zod": "^3.22.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import agent from "../../../utils/agent";
|
|||||||
import sendEmail from "../../../utils/sendEmail";
|
import sendEmail from "../../../utils/sendEmail";
|
||||||
import { verifyParseKey } from "../../../utils/verifyParseKey";
|
import { verifyParseKey } from "../../../utils/verifyParseKey";
|
||||||
|
|
||||||
|
// Allow agent loop to run for up to 5 minutes
|
||||||
|
export const maxDuration = 300;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launches a LangChain agent to process an incoming email,
|
* Launches a LangChain agent to process an incoming email,
|
||||||
* then sends the response to the user.
|
* then sends the response to the user.
|
||||||
@@ -37,6 +40,13 @@ export const POST = async (request: NextRequest) => {
|
|||||||
|
|
||||||
return new NextResponse("ok");
|
return new NextResponse("ok");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
await sendEmail({
|
||||||
|
subject: `Re: ${subject}`,
|
||||||
|
text: "Thanks for using Cal.ai! We're experiencing high demand and can't currently process your request. Please try again later.",
|
||||||
|
to: user.email,
|
||||||
|
from: agentEmail,
|
||||||
|
});
|
||||||
|
|
||||||
return new NextResponse(
|
return new NextResponse(
|
||||||
(error as Error).message || "Something went wrong. Please try again or reach out for help.",
|
(error as Error).message || "Something went wrong. Please try again or reach out for help.",
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import type { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import { env } from "../../../env.mjs";
|
||||||
|
import sendEmail from "../../../utils/sendEmail";
|
||||||
|
|
||||||
|
export const POST = async (request: NextRequest) => {
|
||||||
|
const { userId } = await request.json();
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
select: {
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
username: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return new Response("User not found", { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendEmail({
|
||||||
|
subject: "Welcome to Cal AI",
|
||||||
|
to: user.email,
|
||||||
|
from: `${user.username}@${env.SENDER_DOMAIN}`,
|
||||||
|
text: `Hi ${
|
||||||
|
user.name || `@${user.username}`
|
||||||
|
},\n\nI'm Cal AI, your personal booking assistant! I'll be here, 24/7 to help manage your busy schedule and find times to meet with the people you care about.\n\nHere are some things you can ask me:\n\n- "Book a meeting with @someone" (The @ symbol lets you tag Cal.com users)\n- "What meetings do I have today?" (I'll show you your schedule)\n- "Find a time for coffee with someone@gmail.com" (I'll intro and send them some good times)\n\nI'm still learning, so if you have any feedback, please tweet it to @calcom!\n\nRemember, you can always reach me here, at ${
|
||||||
|
user.username
|
||||||
|
}@${
|
||||||
|
env.SENDER_DOMAIN
|
||||||
|
}.\n\nLooking forward to working together (:\n\n- Cal AI, Your personal booking assistant`,
|
||||||
|
html: `Hi ${
|
||||||
|
user.name || `@${user.username}`
|
||||||
|
},<br><br>I'm Cal AI, your personal booking assistant! I'll be here, 24/7 to help manage your busy schedule and find times to meet with the people you care about.<br><br>Here are some things you can ask me:<br><br>- "Book a meeting with @someone" (The @ symbol lets you tag Cal.com users)<br>- "What meetings do I have today?" (I'll show you your schedule)<br>- "Find a time for coffee with someone@gmail.com" (I'll intro and send them some good times)<br><br>I'm still learning, so if you have any feedback, please send it to <a href="https://twitter.com/calcom">@calcom</a> on X!<br><br>Remember, you can always reach me here, at ${
|
||||||
|
user.username
|
||||||
|
}@${env.SENDER_DOMAIN}.<br><br>Looking forward to working together (:<br><br>- Cal AI`,
|
||||||
|
});
|
||||||
|
return new Response("OK", { status: 200 });
|
||||||
|
};
|
||||||
@@ -3,6 +3,7 @@ import { simpleParser } from "mailparser";
|
|||||||
import type { NextRequest } from "next/server";
|
import type { NextRequest } from "next/server";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { env } from "../../../env.mjs";
|
import { env } from "../../../env.mjs";
|
||||||
@@ -14,6 +15,10 @@ import now from "../../../utils/now";
|
|||||||
import sendEmail from "../../../utils/sendEmail";
|
import sendEmail from "../../../utils/sendEmail";
|
||||||
import { verifyParseKey } from "../../../utils/verifyParseKey";
|
import { verifyParseKey } from "../../../utils/verifyParseKey";
|
||||||
|
|
||||||
|
// Allow receive loop to run for up to 30 seconds
|
||||||
|
// Why so long? the rate determining API call (getAvailability, getEventTypes) can take up to 15 seconds at peak times so we give it a little extra time to complete.
|
||||||
|
export const maxDuration = 30;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies email signature and app authorization,
|
* Verifies email signature and app authorization,
|
||||||
* then hands off to booking agent.
|
* then hands off to booking agent.
|
||||||
@@ -27,18 +32,37 @@ export const POST = async (request: NextRequest) => {
|
|||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const body = Object.fromEntries(formData);
|
const body = Object.fromEntries(formData);
|
||||||
|
|
||||||
// body.dkim looks like {@domain-com.22222222.gappssmtp.com : pass}
|
|
||||||
const signature = (body.dkim as string).includes(" : pass");
|
|
||||||
|
|
||||||
const envelope = JSON.parse(body.envelope as string);
|
const envelope = JSON.parse(body.envelope as string);
|
||||||
|
|
||||||
const aiEmail = envelope.to[0];
|
const aiEmail = envelope.to[0];
|
||||||
|
const subject = body.subject || "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await checkRateLimitAndThrowError({
|
||||||
|
identifier: `ai:email:${envelope.from}`,
|
||||||
|
rateLimitingType: "ai",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
await sendEmail({
|
||||||
|
subject: `Re: ${subject}`,
|
||||||
|
text: "Thanks for using Cal.ai! You've reached your daily limit. Please try again tomorrow.",
|
||||||
|
to: envelope.from,
|
||||||
|
from: aiEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new NextResponse("Exceeded rate limit", { status: 200 }); // Don't return 429 to avoid triggering retry logic in SendGrid
|
||||||
|
}
|
||||||
|
|
||||||
// Parse email from mixed MIME type
|
// Parse email from mixed MIME type
|
||||||
const parsed: ParsedMail = await simpleParser(body.email as Source);
|
const parsed: ParsedMail = await simpleParser(body.email as Source);
|
||||||
|
|
||||||
if (!parsed.text && !parsed.subject) {
|
if (!parsed.text && !parsed.subject) {
|
||||||
|
await sendEmail({
|
||||||
|
subject: `Re: ${subject}`,
|
||||||
|
text: "Thanks for using Cal.ai! It looks like you forgot to include a message. Please try again.",
|
||||||
|
to: envelope.from,
|
||||||
|
from: aiEmail,
|
||||||
|
});
|
||||||
return new NextResponse("Email missing text and subject", { status: 400 });
|
return new NextResponse("Email missing text and subject", { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,14 +79,17 @@ export const POST = async (request: NextRequest) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
where: { email: envelope.from, credentials: { some: { appId: env.APP_ID } } },
|
where: { email: envelope.from },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// body.dkim looks like {@domain-com.22222222.gappssmtp.com : pass}
|
||||||
|
const signature = (body.dkim as string).includes(" : pass");
|
||||||
|
|
||||||
// User is not a cal.com user or is using an unverified email.
|
// User is not a cal.com user or is using an unverified email.
|
||||||
if (!signature || !user) {
|
if (!signature || !user) {
|
||||||
await sendEmail({
|
await sendEmail({
|
||||||
html: `Thanks for your interest in Cal.ai! To get started, Make sure you have a <a href="https://cal.com/signup" target="_blank">cal.com</a> account with this email address.`,
|
html: `Thanks for your interest in Cal.ai! To get started, Make sure you have a <a href="https://cal.com/signup" target="_blank">cal.com</a> account with this email address and then install Cal.ai here: <a href="https://go.cal.com/ai" target="_blank">go.cal.com/ai</a>.`,
|
||||||
subject: `Re: ${body.subject}`,
|
subject: `Re: ${subject}`,
|
||||||
text: `Thanks for your interest in Cal.ai! To get started, Make sure you have a cal.com account with this email address. You can sign up for an account at: https://cal.com/signup`,
|
text: `Thanks for your interest in Cal.ai! To get started, Make sure you have a cal.com account with this email address. You can sign up for an account at: https://cal.com/signup`,
|
||||||
to: envelope.from,
|
to: envelope.from,
|
||||||
from: aiEmail,
|
from: aiEmail,
|
||||||
@@ -79,7 +106,7 @@ export const POST = async (request: NextRequest) => {
|
|||||||
|
|
||||||
await sendEmail({
|
await sendEmail({
|
||||||
html: `Thanks for using Cal.ai! To get started, the app must be installed. <a href=${url} target="_blank">Click this link</a> to install it.`,
|
html: `Thanks for using Cal.ai! To get started, the app must be installed. <a href=${url} target="_blank">Click this link</a> to install it.`,
|
||||||
subject: `Re: ${body.subject}`,
|
subject: `Re: ${subject}`,
|
||||||
text: `Thanks for using Cal.ai! To get started, the app must be installed. Click this link to install the Cal.ai app: ${url}`,
|
text: `Thanks for using Cal.ai! To get started, the app must be installed. Click this link to install the Cal.ai app: ${url}`,
|
||||||
to: envelope.from,
|
to: envelope.from,
|
||||||
from: aiEmail,
|
from: aiEmail,
|
||||||
@@ -106,7 +133,7 @@ export const POST = async (request: NextRequest) => {
|
|||||||
|
|
||||||
if ("error" in availability) {
|
if ("error" in availability) {
|
||||||
await sendEmail({
|
await sendEmail({
|
||||||
subject: `Re: ${body.subject}`,
|
subject: `Re: ${subject}`,
|
||||||
text: "Sorry, there was an error fetching your availability. Please try again.",
|
text: "Sorry, there was an error fetching your availability. Please try again.",
|
||||||
to: user.email,
|
to: user.email,
|
||||||
from: aiEmail,
|
from: aiEmail,
|
||||||
@@ -117,7 +144,7 @@ export const POST = async (request: NextRequest) => {
|
|||||||
|
|
||||||
if ("error" in eventTypes) {
|
if ("error" in eventTypes) {
|
||||||
await sendEmail({
|
await sendEmail({
|
||||||
subject: `Re: ${body.subject}`,
|
subject: `Re: ${subject}`,
|
||||||
text: "Sorry, there was an error fetching your event types. Please try again.",
|
text: "Sorry, there was an error fetching your event types. Please try again.",
|
||||||
to: user.email,
|
to: user.email,
|
||||||
from: aiEmail,
|
from: aiEmail,
|
||||||
@@ -135,8 +162,8 @@ export const POST = async (request: NextRequest) => {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
apiKey,
|
apiKey,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
message: parsed.text,
|
message: parsed.text || "",
|
||||||
subject: parsed.subject,
|
subject: parsed.subject || "",
|
||||||
replyTo: aiEmail,
|
replyTo: aiEmail,
|
||||||
user: {
|
user: {
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export const env = createEnv({
|
|||||||
FRONTEND_URL: process.env.FRONTEND_URL,
|
FRONTEND_URL: process.env.FRONTEND_URL,
|
||||||
APP_ID: process.env.APP_ID,
|
APP_ID: process.env.APP_ID,
|
||||||
APP_URL: process.env.APP_URL,
|
APP_URL: process.env.APP_URL,
|
||||||
|
SENDER_DOMAIN: process.env.SENDER_DOMAIN,
|
||||||
PARSE_KEY: process.env.PARSE_KEY,
|
PARSE_KEY: process.env.PARSE_KEY,
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||||
@@ -36,6 +37,7 @@ export const env = createEnv({
|
|||||||
FRONTEND_URL: z.string().url(),
|
FRONTEND_URL: z.string().url(),
|
||||||
APP_ID: z.string().min(1),
|
APP_ID: z.string().min(1),
|
||||||
APP_URL: z.string().url(),
|
APP_URL: z.string().url(),
|
||||||
|
SENDER_DOMAIN: z.string().min(1),
|
||||||
PARSE_KEY: z.string().min(1),
|
PARSE_KEY: z.string().min(1),
|
||||||
NODE_ENV: z.enum(["development", "test", "production"]),
|
NODE_ENV: z.enum(["development", "test", "production"]),
|
||||||
OPENAI_API_KEY: z.string().min(1),
|
OPENAI_API_KEY: z.string().min(1),
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
@@ -47,7 +47,7 @@ const createBooking = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const responses = {
|
const responses = {
|
||||||
id: invite,
|
id: invite.toString(),
|
||||||
name: user.username,
|
name: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { DynamicStructuredTool } from "langchain/tools";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { env } from "~/src/env.mjs";
|
||||||
|
import type { User, UserList } from "~/src/types/user";
|
||||||
|
import sendEmail from "~/src/utils/sendEmail";
|
||||||
|
|
||||||
|
export const sendBookingEmail = async ({
|
||||||
|
user,
|
||||||
|
agentEmail,
|
||||||
|
subject,
|
||||||
|
to,
|
||||||
|
message,
|
||||||
|
eventTypeSlug,
|
||||||
|
slots,
|
||||||
|
date,
|
||||||
|
}: {
|
||||||
|
apiKey: string;
|
||||||
|
user: User;
|
||||||
|
users: UserList;
|
||||||
|
agentEmail: string;
|
||||||
|
subject: string;
|
||||||
|
to: string;
|
||||||
|
message: string;
|
||||||
|
eventTypeSlug: string;
|
||||||
|
slots?: {
|
||||||
|
time: string;
|
||||||
|
text: string;
|
||||||
|
}[];
|
||||||
|
date: {
|
||||||
|
date: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
|
// const url = `${env.FRONTEND_URL}/${user.username}/${eventTypeSlug}?date=${date}`;
|
||||||
|
const timeUrls = slots?.map(({ time, text }) => {
|
||||||
|
return {
|
||||||
|
url: `${env.FRONTEND_URL}/${user.username}/${eventTypeSlug}?slot=${time}`,
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const dateUrl = {
|
||||||
|
url: `${env.FRONTEND_URL}/${user.username}/${eventTypeSlug}?date=${date.date}`,
|
||||||
|
text: date.text,
|
||||||
|
};
|
||||||
|
|
||||||
|
await sendEmail({
|
||||||
|
subject,
|
||||||
|
to,
|
||||||
|
cc: user.email,
|
||||||
|
from: agentEmail,
|
||||||
|
text: message
|
||||||
|
.split("[[[Slots]]]")
|
||||||
|
.join(timeUrls?.map(({ url, text }) => `${text}: ${url}`).join("\n"))
|
||||||
|
.split("[[[Link]]]")
|
||||||
|
.join(`${dateUrl.text}: ${dateUrl.url}`),
|
||||||
|
html: message
|
||||||
|
.split("\n")
|
||||||
|
.join("<br>")
|
||||||
|
.split("[[[Slots]]]")
|
||||||
|
.join(timeUrls?.map(({ url, text }) => `<a href="${url}">${text}</a>`).join("<br>"))
|
||||||
|
.split("[[[Link]]]")
|
||||||
|
.join(`<a href="${dateUrl.url}">${dateUrl.text}</a>`),
|
||||||
|
});
|
||||||
|
|
||||||
|
return "Booking link sent";
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendBookingEmailTool = (apiKey: string, user: User, users: UserList, agentEmail: string) => {
|
||||||
|
return new DynamicStructuredTool({
|
||||||
|
description:
|
||||||
|
"Send a booking link via email. Useful for scheduling with non cal users. Be confident, suggesting a good date/time with a fallback to a link to select a date/time.",
|
||||||
|
func: async ({ message, subject, to, eventTypeSlug, slots, date }) => {
|
||||||
|
return JSON.stringify(
|
||||||
|
await sendBookingEmail({
|
||||||
|
apiKey,
|
||||||
|
user,
|
||||||
|
users,
|
||||||
|
agentEmail,
|
||||||
|
subject,
|
||||||
|
to,
|
||||||
|
message,
|
||||||
|
eventTypeSlug,
|
||||||
|
slots,
|
||||||
|
date,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
name: "sendBookingEmail",
|
||||||
|
|
||||||
|
schema: z.object({
|
||||||
|
message: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
"A polite and professional email with an intro and signature at the end. Specify you are the AI booking assistant of the primary user. Use [[[Slots]]] and a fallback [[[Link]]] to inject good times and 'see all times' into messages"
|
||||||
|
),
|
||||||
|
subject: z.string(),
|
||||||
|
to: z
|
||||||
|
.string()
|
||||||
|
.describe("email address to send the booking link to. Primary user is automatically CC'd"),
|
||||||
|
eventTypeSlug: z.string().describe("the slug of the event type to book"),
|
||||||
|
slots: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
time: z.string().describe("YYYY-MM-DDTHH:mm in UTC"),
|
||||||
|
text: z.string().describe("minimum readable label. Ex. 4pm."),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.describe("Time slots the external user can click"),
|
||||||
|
date: z
|
||||||
|
.object({
|
||||||
|
date: z.string().describe("YYYY-MM-DD"),
|
||||||
|
text: z.string().describe('"See all times" or similar'),
|
||||||
|
})
|
||||||
|
.describe(
|
||||||
|
"A booking link that allows the external user to select a date / time. Should be a fallback to time slots"
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sendBookingEmailTool;
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import { DynamicStructuredTool } from "langchain/tools";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { env } from "~/src/env.mjs";
|
|
||||||
import type { User, UserList } from "~/src/types/user";
|
|
||||||
import sendEmail from "~/src/utils/sendEmail";
|
|
||||||
|
|
||||||
export const sendBookingLink = async ({
|
|
||||||
user,
|
|
||||||
agentEmail,
|
|
||||||
subject,
|
|
||||||
to,
|
|
||||||
message,
|
|
||||||
eventTypeSlug,
|
|
||||||
date,
|
|
||||||
}: {
|
|
||||||
apiKey: string;
|
|
||||||
user: User;
|
|
||||||
users: UserList;
|
|
||||||
agentEmail: string;
|
|
||||||
subject: string;
|
|
||||||
to: string[];
|
|
||||||
message: string;
|
|
||||||
eventTypeSlug: string;
|
|
||||||
date: string;
|
|
||||||
}) => {
|
|
||||||
const url = `${env.FRONTEND_URL}/${user.username}/${eventTypeSlug}?date=${date}`;
|
|
||||||
|
|
||||||
await sendEmail({
|
|
||||||
subject,
|
|
||||||
to,
|
|
||||||
cc: user.email,
|
|
||||||
from: agentEmail,
|
|
||||||
text: message.split("[[[Booking Link]]]").join(url),
|
|
||||||
html: message
|
|
||||||
.split("\n")
|
|
||||||
.join("<br>")
|
|
||||||
.split("[[[Booking Link]]]")
|
|
||||||
.join(`<a href="${url}">Booking Link</a>`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return "Booking link sent";
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendBookingLinkTool = (apiKey: string, user: User, users: UserList, agentEmail: string) => {
|
|
||||||
return new DynamicStructuredTool({
|
|
||||||
description: "Send a booking link via email. Useful for scheduling with non cal users.",
|
|
||||||
func: async ({ message, subject, to, eventTypeSlug, date }) => {
|
|
||||||
return JSON.stringify(
|
|
||||||
await sendBookingLink({
|
|
||||||
apiKey,
|
|
||||||
user,
|
|
||||||
users,
|
|
||||||
agentEmail,
|
|
||||||
subject,
|
|
||||||
to,
|
|
||||||
message,
|
|
||||||
eventTypeSlug,
|
|
||||||
date,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
name: "sendBookingLink",
|
|
||||||
|
|
||||||
schema: z.object({
|
|
||||||
message: z
|
|
||||||
.string()
|
|
||||||
.describe(
|
|
||||||
"Make sure to nicely format the message and introduce yourself as the primary user's booking assistant. Make sure to include a spot for the link using: [[[Booking Link]]]"
|
|
||||||
),
|
|
||||||
subject: z.string(),
|
|
||||||
to: z
|
|
||||||
.array(z.string())
|
|
||||||
.describe("array of emails to send the booking link to. Primary user is automatically CC'd"),
|
|
||||||
eventTypeSlug: z.string().describe("the slug of the event type to book"),
|
|
||||||
date: z.string().describe("the date (yyyy-mm-dd) to suggest for the booking"),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default sendBookingLinkTool;
|
|
||||||
@@ -6,7 +6,7 @@ import createBookingIfAvailable from "../tools/createBooking";
|
|||||||
import deleteBooking from "../tools/deleteBooking";
|
import deleteBooking from "../tools/deleteBooking";
|
||||||
import getAvailability from "../tools/getAvailability";
|
import getAvailability from "../tools/getAvailability";
|
||||||
import getBookings from "../tools/getBookings";
|
import getBookings from "../tools/getBookings";
|
||||||
import sendBookingLink from "../tools/sendBookingLink";
|
import sendBookingEmail from "../tools/sendBookingEmail";
|
||||||
import updateBooking from "../tools/updateBooking";
|
import updateBooking from "../tools/updateBooking";
|
||||||
import type { EventType } from "../types/eventType";
|
import type { EventType } from "../types/eventType";
|
||||||
import type { User, UserList } from "../types/user";
|
import type { User, UserList } from "../types/user";
|
||||||
@@ -35,7 +35,7 @@ const agent = async (
|
|||||||
createBookingIfAvailable(apiKey, userId, users),
|
createBookingIfAvailable(apiKey, userId, users),
|
||||||
updateBooking(apiKey, userId),
|
updateBooking(apiKey, userId),
|
||||||
deleteBooking(apiKey),
|
deleteBooking(apiKey),
|
||||||
sendBookingLink(apiKey, user, users, agentEmail),
|
sendBookingEmail(apiKey, user, users, agentEmail),
|
||||||
];
|
];
|
||||||
|
|
||||||
const model = new ChatOpenAI({
|
const model = new ChatOpenAI({
|
||||||
@@ -53,6 +53,8 @@ const agent = async (
|
|||||||
Make sure your final answers are definitive, complete and well formatted.
|
Make sure your final answers are definitive, complete and well formatted.
|
||||||
Sometimes, tools return errors. In this case, try to handle the error intelligently or ask the user for more information.
|
Sometimes, tools return errors. In this case, try to handle the error intelligently or ask the user for more information.
|
||||||
Tools will always handle times in UTC, but times sent to users should be formatted per that user's timezone.
|
Tools will always handle times in UTC, but times sent to users should be formatted per that user's timezone.
|
||||||
|
In responses to users, always summarize necessary context and open the door to follow ups. For example "I have booked your chat with @username for 3pm on Wednesday, December 20th, 2023 EST. Please let me know if you need to reschedule."
|
||||||
|
If you can't find a referenced user, ask the user for their email or @username. Make sure to specify that usernames require the @username format. Users don't know other users' userIds.
|
||||||
|
|
||||||
The primary user's id is: ${userId}
|
The primary user's id is: ${userId}
|
||||||
The primary user's username is: ${user.username}
|
The primary user's username is: ${user.username}
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ import type { UserList } from "../types/user";
|
|||||||
* Extracts usernames (@Example) and emails (hi@example.com) from a string
|
* Extracts usernames (@Example) and emails (hi@example.com) from a string
|
||||||
*/
|
*/
|
||||||
export const extractUsers = async (text: string) => {
|
export const extractUsers = async (text: string) => {
|
||||||
const usernames = text.match(/(?<![a-zA-Z0-9_.])@[a-zA-Z0-9_]+/g)?.map((username) => username.slice(1));
|
const usernames = text
|
||||||
const emails = text.match(/[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+/g);
|
.match(/(?<![a-zA-Z0-9_.])@[a-zA-Z0-9_]+/g)
|
||||||
|
?.map((username) => username.slice(1).toLowerCase());
|
||||||
|
const emails = text
|
||||||
|
.match(/[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+/g)
|
||||||
|
?.map((email) => email.toLowerCase());
|
||||||
|
|
||||||
const dbUsersFromUsernames = usernames
|
const dbUsersFromUsernames = usernames
|
||||||
? await prisma.user.findMany({
|
? await prisma.user.findMany({
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { NextMiddleware } from "next-api-middleware";
|
||||||
|
|
||||||
|
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||||
|
|
||||||
|
export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
|
||||||
|
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
|
||||||
|
|
||||||
|
// TODO: Add a way to add trusted api keys
|
||||||
|
await checkRateLimitAndThrowError({
|
||||||
|
identifier: req.query.apiKey as string,
|
||||||
|
rateLimitingType: "api",
|
||||||
|
});
|
||||||
|
|
||||||
|
await next();
|
||||||
|
};
|
||||||
@@ -4,7 +4,7 @@ import { hashAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
|
|||||||
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
|
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
|
||||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||||
|
|
||||||
import { isAdminGuard } from "~/lib/utils/isAdmin";
|
import { isAdminGuard } from "../utils/isAdmin";
|
||||||
|
|
||||||
// Used to check if the apiKey is not expired, could be extracted if reused. but not for now.
|
// Used to check if the apiKey is not expired, could be extracted if reused. but not for now.
|
||||||
export const dateNotInPast = function (date: Date) {
|
export const dateNotInPast = function (date: Date) {
|
||||||
|
|||||||
@@ -12,24 +12,29 @@ import {
|
|||||||
HTTP_GET_OR_POST,
|
HTTP_GET_OR_POST,
|
||||||
HTTP_GET_DELETE_PATCH,
|
HTTP_GET_DELETE_PATCH,
|
||||||
} from "./httpMethods";
|
} from "./httpMethods";
|
||||||
|
import { rateLimitApiKey } from "./rateLimitApiKey";
|
||||||
import { verifyApiKey } from "./verifyApiKey";
|
import { verifyApiKey } from "./verifyApiKey";
|
||||||
import { withPagination } from "./withPagination";
|
import { withPagination } from "./withPagination";
|
||||||
|
|
||||||
const withMiddleware = label(
|
const middleware = {
|
||||||
{
|
HTTP_GET_OR_POST,
|
||||||
HTTP_GET_OR_POST,
|
HTTP_GET_DELETE_PATCH,
|
||||||
HTTP_GET_DELETE_PATCH,
|
HTTP_GET,
|
||||||
HTTP_GET,
|
HTTP_PATCH,
|
||||||
HTTP_PATCH,
|
HTTP_POST,
|
||||||
HTTP_POST,
|
HTTP_DELETE,
|
||||||
HTTP_DELETE,
|
addRequestId,
|
||||||
addRequestId,
|
verifyApiKey,
|
||||||
verifyApiKey,
|
rateLimitApiKey,
|
||||||
customPrismaClient,
|
customPrismaClient,
|
||||||
extendRequest,
|
extendRequest,
|
||||||
pagination: withPagination,
|
pagination: withPagination,
|
||||||
captureErrors,
|
captureErrors,
|
||||||
},
|
};
|
||||||
|
|
||||||
|
type Middleware = keyof typeof middleware;
|
||||||
|
|
||||||
|
const middlewareOrder =
|
||||||
// The order here, determines the order of execution
|
// The order here, determines the order of execution
|
||||||
[
|
[
|
||||||
"extendRequest",
|
"extendRequest",
|
||||||
@@ -37,8 +42,10 @@ const withMiddleware = label(
|
|||||||
// - Put customPrismaClient before verifyApiKey always.
|
// - Put customPrismaClient before verifyApiKey always.
|
||||||
"customPrismaClient",
|
"customPrismaClient",
|
||||||
"verifyApiKey",
|
"verifyApiKey",
|
||||||
|
"rateLimitApiKey",
|
||||||
"addRequestId",
|
"addRequestId",
|
||||||
] // <-- Provide a list of middleware to call automatically
|
] as Middleware[]; // <-- Provide a list of middleware to call automatically
|
||||||
);
|
|
||||||
|
|
||||||
export { withMiddleware };
|
const withMiddleware = label(middleware, middlewareOrder);
|
||||||
|
|
||||||
|
export { withMiddleware, middleware, middlewareOrder };
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
|
||||||
|
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||||
|
|
||||||
|
export function extractUserIdsFromQuery({ isAdmin, query }: NextApiRequest) {
|
||||||
|
/** Guard: Only admins can query other users */
|
||||||
|
if (!isAdmin) {
|
||||||
|
throw new HttpError({ statusCode: 401, message: "ADMIN required" });
|
||||||
|
}
|
||||||
|
const { userId: userIdOrUserIds } = schemaQuerySingleOrMultipleUserIds.parse(query);
|
||||||
|
return Array.isArray(userIdOrUserIds) ? userIdOrUserIds : [userIdOrUserIds];
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export function isValidBase64Image(input: string): boolean {
|
||||||
|
const regex = /^data:image\/[^;]+;base64,(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
||||||
|
return regex.test(input);
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ const schemaAvailabilityCreateParams = z
|
|||||||
startTime: z.date().or(z.string()),
|
startTime: z.date().or(z.string()),
|
||||||
endTime: z.date().or(z.string()),
|
endTime: z.date().or(z.string()),
|
||||||
days: z.array(z.number()).optional(),
|
days: z.array(z.number()).optional(),
|
||||||
|
date: z.date().or(z.string()).optional(),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ const schemaAvailabilityEditParams = z
|
|||||||
startTime: z.date().or(z.string()).optional(),
|
startTime: z.date().or(z.string()).optional(),
|
||||||
endTime: z.date().or(z.string()).optional(),
|
endTime: z.date().or(z.string()).optional(),
|
||||||
days: z.array(z.number()).optional(),
|
days: z.array(z.number()).optional(),
|
||||||
|
date: z.date().or(z.string()).optional(),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ const schemaDestinationCalendarCreateParams = z
|
|||||||
.object({
|
.object({
|
||||||
integration: z.string(),
|
integration: z.string(),
|
||||||
externalId: z.string(),
|
externalId: z.string(),
|
||||||
eventTypeId: z.number(),
|
eventTypeId: z.number().optional(),
|
||||||
bookingId: z.number(),
|
bookingId: z.number().optional(),
|
||||||
userId: z.number(),
|
userId: z.number().optional(),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
|
|||||||
successRedirectUrl: true,
|
successRedirectUrl: true,
|
||||||
locations: true,
|
locations: true,
|
||||||
bookingLimits: true,
|
bookingLimits: true,
|
||||||
|
onlyShowFirstAvailableSlot: true,
|
||||||
durationLimits: true,
|
durationLimits: true,
|
||||||
})
|
})
|
||||||
.merge(
|
.merge(
|
||||||
@@ -147,6 +148,7 @@ export const schemaEventTypeReadPublic = EventType.pick({
|
|||||||
seatsShowAvailabilityCount: true,
|
seatsShowAvailabilityCount: true,
|
||||||
bookingFields: true,
|
bookingFields: true,
|
||||||
bookingLimits: true,
|
bookingLimits: true,
|
||||||
|
onlyShowFirstAvailableSlot: true,
|
||||||
durationLimits: true,
|
durationLimits: true,
|
||||||
}).merge(
|
}).merge(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -21,7 +21,16 @@ export const schemaSchedulePublic = z
|
|||||||
.merge(
|
.merge(
|
||||||
z.object({
|
z.object({
|
||||||
availability: z
|
availability: z
|
||||||
.array(Availability.pick({ id: true, eventTypeId: true, days: true, startTime: true, endTime: true }))
|
.array(
|
||||||
|
Availability.pick({
|
||||||
|
id: true,
|
||||||
|
eventTypeId: true,
|
||||||
|
date: true,
|
||||||
|
days: true,
|
||||||
|
startTime: true,
|
||||||
|
endTime: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
.transform((v) =>
|
.transform((v) =>
|
||||||
v.map((item) => ({
|
v.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { checkUsername } from "@calcom/lib/server/checkUsername";
|
|||||||
import { _UserModel as User } from "@calcom/prisma/zod";
|
import { _UserModel as User } from "@calcom/prisma/zod";
|
||||||
import { iso8601 } from "@calcom/prisma/zod-utils";
|
import { iso8601 } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
|
import { isValidBase64Image } from "~/lib/utils/isValidBase64Image";
|
||||||
import { timeZone } from "~/lib/validations/shared/timeZone";
|
import { timeZone } from "~/lib/validations/shared/timeZone";
|
||||||
|
|
||||||
// @note: These are the ONLY values allowed as weekStart. So user don't introduce bad data.
|
// @note: These are the ONLY values allowed as weekStart. So user don't introduce bad data.
|
||||||
@@ -75,6 +76,7 @@ export const schemaUserBaseBodyParams = User.pick({
|
|||||||
theme: true,
|
theme: true,
|
||||||
defaultScheduleId: true,
|
defaultScheduleId: true,
|
||||||
locale: true,
|
locale: true,
|
||||||
|
hideBranding: true,
|
||||||
timeFormat: true,
|
timeFormat: true,
|
||||||
brandColor: true,
|
brandColor: true,
|
||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
@@ -90,11 +92,12 @@ export const schemaUserBaseBodyParams = User.pick({
|
|||||||
// Here we can both require or not (adding optional or nullish) and also rewrite validations for any value
|
// Here we can both require or not (adding optional or nullish) and also rewrite validations for any value
|
||||||
// for example making weekStart only accept weekdays as input
|
// for example making weekStart only accept weekdays as input
|
||||||
const schemaUserEditParams = z.object({
|
const schemaUserEditParams = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email().toLowerCase(),
|
||||||
username: usernameSchema,
|
username: usernameSchema,
|
||||||
weekStart: z.nativeEnum(weekdays).optional(),
|
weekStart: z.nativeEnum(weekdays).optional(),
|
||||||
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||||
darkBrandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
darkBrandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||||
|
hideBranding: z.boolean().optional(),
|
||||||
timeZone: timeZone.optional(),
|
timeZone: timeZone.optional(),
|
||||||
theme: z.nativeEnum(theme).optional().nullable(),
|
theme: z.nativeEnum(theme).optional().nullable(),
|
||||||
timeFormat: z.nativeEnum(timeFormat).optional(),
|
timeFormat: z.nativeEnum(timeFormat).optional(),
|
||||||
@@ -104,17 +107,19 @@ const schemaUserEditParams = z.object({
|
|||||||
.optional()
|
.optional()
|
||||||
.nullable(),
|
.nullable(),
|
||||||
locale: z.nativeEnum(locales).optional().nullable(),
|
locale: z.nativeEnum(locales).optional().nullable(),
|
||||||
|
avatar: z.string().refine(isValidBase64Image).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// @note: These are the values that are editable via PATCH method on the user Model,
|
// @note: These are the values that are editable via PATCH method on the user Model,
|
||||||
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
|
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
|
||||||
|
|
||||||
const schemaUserCreateParams = z.object({
|
const schemaUserCreateParams = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email().toLowerCase(),
|
||||||
username: usernameSchema,
|
username: usernameSchema,
|
||||||
weekStart: z.nativeEnum(weekdays).optional(),
|
weekStart: z.nativeEnum(weekdays).optional(),
|
||||||
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||||
darkBrandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
darkBrandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||||
|
hideBranding: z.boolean().optional(),
|
||||||
timeZone: timeZone.optional(),
|
timeZone: timeZone.optional(),
|
||||||
theme: z.nativeEnum(theme).optional().nullable(),
|
theme: z.nativeEnum(theme).optional().nullable(),
|
||||||
timeFormat: z.nativeEnum(timeFormat).optional(),
|
timeFormat: z.nativeEnum(timeFormat).optional(),
|
||||||
@@ -125,6 +130,7 @@ const schemaUserCreateParams = z.object({
|
|||||||
.nullable(),
|
.nullable(),
|
||||||
locale: z.nativeEnum(locales).optional(),
|
locale: z.nativeEnum(locales).optional(),
|
||||||
createdDate: iso8601.optional(),
|
createdDate: iso8601.optional(),
|
||||||
|
avatar: z.string().refine(isValidBase64Image).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// @note: These are the values that are editable via PATCH method on the user Model,
|
// @note: These are the values that are editable via PATCH method on the user Model,
|
||||||
@@ -157,6 +163,7 @@ export const schemaUserReadPublic = User.pick({
|
|||||||
defaultScheduleId: true,
|
defaultScheduleId: true,
|
||||||
locale: true,
|
locale: true,
|
||||||
timeFormat: true,
|
timeFormat: true,
|
||||||
|
hideBranding: true,
|
||||||
brandColor: true,
|
brandColor: true,
|
||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
allowDynamicBooking: true,
|
allowDynamicBooking: true,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export const schemaWebhookCreateParams = z
|
|||||||
payloadTemplate: z.string().optional().nullable(),
|
payloadTemplate: z.string().optional().nullable(),
|
||||||
eventTypeId: z.number().optional(),
|
eventTypeId: z.number().optional(),
|
||||||
userId: z.number().optional(),
|
userId: z.number().optional(),
|
||||||
|
secret: z.string().optional().nullable(),
|
||||||
// API shouldn't mess with Apps webhooks yet (ie. Zapier)
|
// API shouldn't mess with Apps webhooks yet (ie. Zapier)
|
||||||
// appId: z.string().optional().nullable(),
|
// appId: z.string().optional().nullable(),
|
||||||
})
|
})
|
||||||
@@ -31,6 +32,7 @@ export const schemaWebhookEditBodyParams = schemaWebhookBaseBodyParams
|
|||||||
.merge(
|
.merge(
|
||||||
z.object({
|
z.object({
|
||||||
eventTriggers: z.enum(WEBHOOK_TRIGGER_EVENTS).array().optional(),
|
eventTriggers: z.enum(WEBHOOK_TRIGGER_EVENTS).array().optional(),
|
||||||
|
secret: z.string().optional().nullable(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.partial()
|
.partial()
|
||||||
|
|||||||
+15
-2
@@ -1,6 +1,8 @@
|
|||||||
const { withAxiom } = require("next-axiom");
|
const { withAxiom } = require("next-axiom");
|
||||||
|
const { withSentryConfig } = require("@sentry/nextjs");
|
||||||
|
|
||||||
module.exports = withAxiom({
|
const plugins = [withAxiom];
|
||||||
|
const nextConfig = {
|
||||||
transpilePackages: [
|
transpilePackages: [
|
||||||
"@calcom/app-store",
|
"@calcom/app-store",
|
||||||
"@calcom/core",
|
"@calcom/core",
|
||||||
@@ -66,4 +68,15 @@ module.exports = withAxiom({
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||||
|
nextConfig["sentry"] = {
|
||||||
|
autoInstrumentServerFunctions: true,
|
||||||
|
hideSourceMaps: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
plugins.push(withSentryConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"lint": "eslint . --ignore-path .gitignore",
|
"lint": "eslint . --ignore-path .gitignore",
|
||||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||||
"start": "PORT=3002 next start",
|
"start": "PORT=3002 next start",
|
||||||
|
"docker-start-api": "PORT=80 next start",
|
||||||
"type-check": "tsc --pretty --noEmit"
|
"type-check": "tsc --pretty --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -29,7 +30,7 @@
|
|||||||
"@calcom/lib": "*",
|
"@calcom/lib": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
"@calcom/trpc": "*",
|
"@calcom/trpc": "*",
|
||||||
"@sentry/nextjs": "^7.20.0",
|
"@sentry/nextjs": "^7.73.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"next": "^13.4.6",
|
"next": "^13.4.6",
|
||||||
|
|||||||
@@ -6,18 +6,44 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
|||||||
|
|
||||||
async function authMiddleware(req: NextApiRequest) {
|
async function authMiddleware(req: NextApiRequest) {
|
||||||
const { userId, prisma, isAdmin, query } = req;
|
const { userId, prisma, isAdmin, query } = req;
|
||||||
|
if (isAdmin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { id } = schemaQueryIdParseInt.parse(query);
|
const { id } = schemaQueryIdParseInt.parse(query);
|
||||||
const userWithBookings = await prisma.user.findUnique({
|
const userWithBookingsAndTeamIds = await prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
include: { bookings: true },
|
include: {
|
||||||
|
bookings: true,
|
||||||
|
teams: {
|
||||||
|
select: {
|
||||||
|
teamId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!userWithBookings) throw new HttpError({ statusCode: 404, message: "User not found" });
|
if (!userWithBookingsAndTeamIds) throw new HttpError({ statusCode: 404, message: "User not found" });
|
||||||
|
|
||||||
const userBookingIds = userWithBookings.bookings.map((booking) => booking.id);
|
const userBookingIds = userWithBookingsAndTeamIds.bookings.map((booking) => booking.id);
|
||||||
|
|
||||||
if (!isAdmin && !userBookingIds.includes(id)) {
|
if (!userBookingIds.includes(id)) {
|
||||||
throw new HttpError({ statusCode: 401, message: "You are not authorized" });
|
const teamBookings = await prisma.booking.findUnique({
|
||||||
|
where: {
|
||||||
|
id: id,
|
||||||
|
eventType: {
|
||||||
|
team: {
|
||||||
|
id: {
|
||||||
|
in: userWithBookingsAndTeamIds.teams.map((team) => team.teamId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!teamBookings) {
|
||||||
|
throw new HttpError({ statusCode: 401, message: "You are not authorized" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
|||||||
* type: boolean
|
* type: boolean
|
||||||
* description: Delete all remaining bookings
|
* description: Delete all remaining bookings
|
||||||
* - in: query
|
* - in: query
|
||||||
* name: reason
|
* name: cancellationReason
|
||||||
* required: false
|
* required: false
|
||||||
* schema:
|
* schema:
|
||||||
* type: string
|
* type: string
|
||||||
@@ -56,10 +56,6 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
|||||||
* <td>The provided id didn't correspond to any existing booking.</td>
|
* <td>The provided id didn't correspond to any existing booking.</td>
|
||||||
* </tr>
|
* </tr>
|
||||||
* <tr>
|
* <tr>
|
||||||
* <td>Cannot cancel past events</td>
|
|
||||||
* <td>The provided id matched an existing booking with a past startDate.</td>
|
|
||||||
* </tr>
|
|
||||||
* <tr>
|
|
||||||
* <td>User not found</td>
|
* <td>User not found</td>
|
||||||
* <td>The userId did not matched an existing user.</td>
|
* <td>The userId did not matched an existing user.</td>
|
||||||
* </tr>
|
* </tr>
|
||||||
|
|||||||
@@ -205,8 +205,8 @@ import { defaultResponder } from "@calcom/lib/server";
|
|||||||
async function handler(req: NextApiRequest) {
|
async function handler(req: NextApiRequest) {
|
||||||
const { userId, isAdmin } = req;
|
const { userId, isAdmin } = req;
|
||||||
if (isAdmin) req.userId = req.body.userId || userId;
|
if (isAdmin) req.userId = req.body.userId || userId;
|
||||||
const booking = await handleNewBooking(req);
|
|
||||||
return booking;
|
return await handleNewBooking(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defaultResponder(handler);
|
export default defaultResponder(handler);
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
|
|
||||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
|
||||||
import type { DestinationCalendarResponse } from "~/lib/types";
|
|
||||||
import {
|
|
||||||
schemaDestinationCalendarEditBodyParams,
|
|
||||||
schemaDestinationCalendarReadPublic,
|
|
||||||
} from "~/lib/validations/destination-calendar";
|
|
||||||
import {
|
|
||||||
schemaQueryIdParseInt,
|
|
||||||
withValidQueryIdTransformParseInt,
|
|
||||||
} from "~/lib/validations/shared/queryIdTransformParseInt";
|
|
||||||
|
|
||||||
export async function destionationCalendarById(
|
|
||||||
{ method, query, body, userId, prisma }: NextApiRequest,
|
|
||||||
res: NextApiResponse<DestinationCalendarResponse>
|
|
||||||
) {
|
|
||||||
const safeQuery = schemaQueryIdParseInt.safeParse(query);
|
|
||||||
const safeBody = schemaDestinationCalendarEditBodyParams.safeParse(body);
|
|
||||||
if (!safeQuery.success) {
|
|
||||||
res.status(400).json({ message: "Your query was invalid" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = await prisma.destinationCalendar.findMany({ where: { userId } });
|
|
||||||
const userDestinationCalendars = data.map((destinationCalendar) => destinationCalendar.id);
|
|
||||||
// FIXME: Should we also check ownership of bokingId and eventTypeId to avoid users cross-pollinating other users calendars.
|
|
||||||
// On a related note, moving from sequential integer IDs to UUIDs would be a good idea. and maybe help avoid having this problem.
|
|
||||||
if (userDestinationCalendars.includes(safeQuery.data.id)) res.status(401).json({ message: "Unauthorized" });
|
|
||||||
else {
|
|
||||||
switch (method) {
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /destination-calendars/{id}:
|
|
||||||
* get:
|
|
||||||
* summary: Find a destination calendar
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: id
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* required: true
|
|
||||||
* description: ID of the destination calendar to get
|
|
||||||
* - in: query
|
|
||||||
* name: apiKey
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* description: Your API key
|
|
||||||
* tags:
|
|
||||||
* - destination-calendars
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: OK
|
|
||||||
* 401:
|
|
||||||
* description: Authorization information is missing or invalid.
|
|
||||||
* 404:
|
|
||||||
* description: DestinationCalendar was not found
|
|
||||||
* patch:
|
|
||||||
* summary: Edit an existing destination calendar
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: id
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* required: true
|
|
||||||
* description: ID of the destination calendar to edit
|
|
||||||
* - in: query
|
|
||||||
* name: apiKey
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* description: Your API key
|
|
||||||
* requestBody:
|
|
||||||
* description: Create a new booking related to one of your event-types
|
|
||||||
* required: true
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* integration:
|
|
||||||
* type: string
|
|
||||||
* description: 'The integration'
|
|
||||||
* externalId:
|
|
||||||
* type: string
|
|
||||||
* description: 'The external ID of the integration'
|
|
||||||
* eventTypeId:
|
|
||||||
* type: integer
|
|
||||||
* description: 'The ID of the eventType it is associated with'
|
|
||||||
* bookingId:
|
|
||||||
* type: integer
|
|
||||||
* description: 'The booking ID it is associated with'
|
|
||||||
* tags:
|
|
||||||
* - destination-calendars
|
|
||||||
* responses:
|
|
||||||
* 201:
|
|
||||||
* description: OK, destinationCalendar edited successfuly
|
|
||||||
* 400:
|
|
||||||
* description: Bad request. DestinationCalendar body is invalid.
|
|
||||||
* 401:
|
|
||||||
* description: Authorization information is missing or invalid.
|
|
||||||
* delete:
|
|
||||||
* summary: Remove an existing destination calendar
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: id
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* required: true
|
|
||||||
* description: ID of the destination calendar to delete
|
|
||||||
* - in: query
|
|
||||||
* name: apiKey
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* description: Your API key
|
|
||||||
* tags:
|
|
||||||
* - destination-calendars
|
|
||||||
* responses:
|
|
||||||
* 201:
|
|
||||||
* description: OK, destinationCalendar removed successfuly
|
|
||||||
* 400:
|
|
||||||
* description: Bad request. DestinationCalendar id is invalid.
|
|
||||||
* 401:
|
|
||||||
* description: Authorization information is missing or invalid.
|
|
||||||
*/
|
|
||||||
case "GET":
|
|
||||||
await prisma.destinationCalendar
|
|
||||||
.findUnique({ where: { id: safeQuery.data.id } })
|
|
||||||
.then((data) => schemaDestinationCalendarReadPublic.parse(data))
|
|
||||||
.then((destination_calendar) => res.status(200).json({ destination_calendar }))
|
|
||||||
.catch((error: Error) =>
|
|
||||||
res.status(404).json({
|
|
||||||
message: `DestinationCalendar with id: ${safeQuery.data.id} not found`,
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /destination-calendars/{id}:
|
|
||||||
* patch:
|
|
||||||
* summary: Edit an existing destination calendar
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: id
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* required: true
|
|
||||||
* description: ID of the destination calendar to edit
|
|
||||||
* - in: query
|
|
||||||
* name: apiKey
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* description: Your API key
|
|
||||||
* tags:
|
|
||||||
* - destination-calendars
|
|
||||||
* responses:
|
|
||||||
* 201:
|
|
||||||
* description: OK, destinationCalendar edited successfuly
|
|
||||||
* 400:
|
|
||||||
* description: Bad request. DestinationCalendar body is invalid.
|
|
||||||
* 401:
|
|
||||||
* description: Authorization information is missing or invalid.
|
|
||||||
*/
|
|
||||||
case "PATCH":
|
|
||||||
if (!safeBody.success) {
|
|
||||||
{
|
|
||||||
res.status(400).json({ message: "Invalid request body" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await prisma.destinationCalendar
|
|
||||||
.update({ where: { id: safeQuery.data.id }, data: safeBody.data })
|
|
||||||
.then((data) => schemaDestinationCalendarReadPublic.parse(data))
|
|
||||||
.then((destination_calendar) => res.status(200).json({ destination_calendar }))
|
|
||||||
.catch((error: Error) =>
|
|
||||||
res.status(404).json({
|
|
||||||
message: `DestinationCalendar with id: ${safeQuery.data.id} not found`,
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /destination-calendars/{id}:
|
|
||||||
* delete:
|
|
||||||
* summary: Remove an existing destination calendar
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: id
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* required: true
|
|
||||||
* description: ID of the destination calendar to delete
|
|
||||||
* - in: query
|
|
||||||
* name: apiKey
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* description: Your API key
|
|
||||||
* tags:
|
|
||||||
* - destination-calendars
|
|
||||||
* responses:
|
|
||||||
* 201:
|
|
||||||
* description: OK, destinationCalendar removed successfuly
|
|
||||||
* 400:
|
|
||||||
* description: Bad request. DestinationCalendar id is invalid.
|
|
||||||
* 401:
|
|
||||||
* description: Authorization information is missing or invalid.
|
|
||||||
*/
|
|
||||||
case "DELETE":
|
|
||||||
await prisma.destinationCalendar
|
|
||||||
.delete({
|
|
||||||
where: { id: safeQuery.data.id },
|
|
||||||
})
|
|
||||||
.then(() =>
|
|
||||||
res.status(200).json({
|
|
||||||
message: `DestinationCalendar with id: ${safeQuery.data.id} deleted`,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch((error: Error) =>
|
|
||||||
res.status(404).json({
|
|
||||||
message: `DestinationCalendar with id: ${safeQuery.data.id} not found`,
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
res.status(405).json({ message: "Method not allowed" });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withMiddleware("HTTP_GET_DELETE_PATCH")(
|
|
||||||
withValidQueryIdTransformParseInt(destionationCalendarById)
|
|
||||||
);
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
|
||||||
|
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||||
|
|
||||||
|
async function authMiddleware(req: NextApiRequest) {
|
||||||
|
const { userId, isAdmin, prisma } = req;
|
||||||
|
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||||
|
if (isAdmin) return;
|
||||||
|
const userEventTypes = await prisma.eventType.findMany({
|
||||||
|
where: { userId },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const userEventTypeIds = userEventTypes.map((eventType) => eventType.id);
|
||||||
|
|
||||||
|
const destinationCalendar = await prisma.destinationCalendar.findFirst({
|
||||||
|
where: {
|
||||||
|
AND: [
|
||||||
|
{ id },
|
||||||
|
{
|
||||||
|
OR: [{ userId }, { eventTypeId: { in: userEventTypeIds } }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!destinationCalendar)
|
||||||
|
throw new HttpError({ statusCode: 404, message: "Destination calendar not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default authMiddleware;
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
|
||||||
|
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /destination-calendars/{id}:
|
||||||
|
* delete:
|
||||||
|
* summary: Remove an existing destination calendar
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* required: true
|
||||||
|
* description: ID of the destination calendar to delete
|
||||||
|
* - in: query
|
||||||
|
* name: apiKey
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Your API key
|
||||||
|
* tags:
|
||||||
|
* - destination-calendars
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: OK, destinationCalendar removed successfully
|
||||||
|
* 401:
|
||||||
|
* description: Authorization information is missing or invalid.
|
||||||
|
* 404:
|
||||||
|
* description: Destination calendar not found
|
||||||
|
*/
|
||||||
|
export async function deleteHandler(req: NextApiRequest) {
|
||||||
|
const { prisma, query } = req;
|
||||||
|
const { id } = schemaQueryIdParseInt.parse(query);
|
||||||
|
await prisma.destinationCalendar.delete({ where: { id } });
|
||||||
|
return { message: `OK, Destination Calendar removed successfully` };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultResponder(deleteHandler);
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
|
||||||
|
import { schemaDestinationCalendarReadPublic } from "~/lib/validations/destination-calendar";
|
||||||
|
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /destination-calendars/{id}:
|
||||||
|
* get:
|
||||||
|
* summary: Find a destination calendar
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* required: true
|
||||||
|
* description: ID of the destination calendar to get
|
||||||
|
* - in: query
|
||||||
|
* name: apiKey
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Your API key
|
||||||
|
* tags:
|
||||||
|
* - destination-calendars
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: OK
|
||||||
|
* 401:
|
||||||
|
* description: Authorization information is missing or invalid.
|
||||||
|
* 404:
|
||||||
|
* description: Destination calendar not found
|
||||||
|
*/
|
||||||
|
export async function getHandler(req: NextApiRequest) {
|
||||||
|
const { prisma, query } = req;
|
||||||
|
const { id } = schemaQueryIdParseInt.parse(query);
|
||||||
|
|
||||||
|
const destinationCalendar = await prisma.destinationCalendar.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { destinationCalendar: schemaDestinationCalendarReadPublic.parse({ ...destinationCalendar }) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultResponder(getHandler);
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
import type { Prisma } from "@prisma/client";
|
||||||
|
import type { NextApiRequest } from "next";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
|
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||||
|
|
||||||
|
import {
|
||||||
|
schemaDestinationCalendarEditBodyParams,
|
||||||
|
schemaDestinationCalendarReadPublic,
|
||||||
|
} from "~/lib/validations/destination-calendar";
|
||||||
|
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /destination-calendars/{id}:
|
||||||
|
* patch:
|
||||||
|
* summary: Edit an existing destination calendar
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* required: true
|
||||||
|
* description: ID of the destination calendar to edit
|
||||||
|
* - in: query
|
||||||
|
* name: apiKey
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Your API key
|
||||||
|
* requestBody:
|
||||||
|
* description: Create a new booking related to one of your event-types
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* integration:
|
||||||
|
* type: string
|
||||||
|
* description: 'The integration'
|
||||||
|
* externalId:
|
||||||
|
* type: string
|
||||||
|
* description: 'The external ID of the integration'
|
||||||
|
* eventTypeId:
|
||||||
|
* type: integer
|
||||||
|
* description: 'The ID of the eventType it is associated with'
|
||||||
|
* bookingId:
|
||||||
|
* type: integer
|
||||||
|
* description: 'The booking ID it is associated with'
|
||||||
|
* tags:
|
||||||
|
* - destination-calendars
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: OK
|
||||||
|
* 401:
|
||||||
|
* description: Authorization information is missing or invalid.
|
||||||
|
* 404:
|
||||||
|
* description: Destination calendar not found
|
||||||
|
*/
|
||||||
|
type DestinationCalendarType = {
|
||||||
|
userId?: number | null;
|
||||||
|
eventTypeId?: number | null;
|
||||||
|
credentialId: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UserCredentialType = {
|
||||||
|
id: number;
|
||||||
|
appId: string | null;
|
||||||
|
type: string;
|
||||||
|
userId: number | null;
|
||||||
|
user: {
|
||||||
|
email: string;
|
||||||
|
} | null;
|
||||||
|
teamId: number | null;
|
||||||
|
key: Prisma.JsonValue;
|
||||||
|
invalid: boolean | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function patchHandler(req: NextApiRequest) {
|
||||||
|
const { userId, isAdmin, prisma, query, body } = req;
|
||||||
|
const { id } = schemaQueryIdParseInt.parse(query);
|
||||||
|
const parsedBody = schemaDestinationCalendarEditBodyParams.parse(body);
|
||||||
|
const assignedUserId = isAdmin ? parsedBody.userId || userId : userId;
|
||||||
|
|
||||||
|
validateIntegrationInput(parsedBody);
|
||||||
|
const destinationCalendarObject: DestinationCalendarType = await getDestinationCalendar(id, prisma);
|
||||||
|
await validateRequestAndOwnership({ destinationCalendarObject, parsedBody, assignedUserId, prisma });
|
||||||
|
|
||||||
|
const userCredentials = await getUserCredentials({
|
||||||
|
credentialId: destinationCalendarObject.credentialId,
|
||||||
|
userId: assignedUserId,
|
||||||
|
prisma,
|
||||||
|
});
|
||||||
|
const credentialId = await verifyCredentialsAndGetId({
|
||||||
|
parsedBody,
|
||||||
|
userCredentials,
|
||||||
|
currentCredentialId: destinationCalendarObject.credentialId,
|
||||||
|
});
|
||||||
|
// If the user has passed eventTypeId, we need to remove userId from the update data to make sure we don't link it to user as well
|
||||||
|
if (parsedBody.eventTypeId) parsedBody.userId = undefined;
|
||||||
|
const destinationCalendar = await prisma.destinationCalendar.update({
|
||||||
|
where: { id },
|
||||||
|
data: { ...parsedBody, credentialId },
|
||||||
|
});
|
||||||
|
return { destinationCalendar: schemaDestinationCalendarReadPublic.parse(destinationCalendar) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves user credentials associated with a given credential ID and user ID and validates if the credentials belong to this user
|
||||||
|
*
|
||||||
|
* @param credentialId - The ID of the credential to fetch. If not provided, an error is thrown.
|
||||||
|
* @param userId - The user ID against which the credentials need to be verified.
|
||||||
|
* @param prisma - An instance of PrismaClient for database operations.
|
||||||
|
*
|
||||||
|
* @returns - An array containing the matching user credentials.
|
||||||
|
*
|
||||||
|
* @throws HttpError - If `credentialId` is not provided or no associated credentials are found in the database.
|
||||||
|
*/
|
||||||
|
async function getUserCredentials({
|
||||||
|
credentialId,
|
||||||
|
userId,
|
||||||
|
prisma,
|
||||||
|
}: {
|
||||||
|
credentialId: number | null;
|
||||||
|
userId: number;
|
||||||
|
prisma: PrismaClient;
|
||||||
|
}) {
|
||||||
|
if (!credentialId) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: `Destination calendar missing credential id`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const userCredentials = await prisma.credential.findMany({
|
||||||
|
where: { id: credentialId, userId },
|
||||||
|
select: credentialForCalendarServiceSelect,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userCredentials || userCredentials.length === 0) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: `Bad request, no associated credentials found`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return userCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the provided credentials and retrieves the associated credential ID.
|
||||||
|
*
|
||||||
|
* This function checks if the `integration` and `externalId` properties from the parsed body are present.
|
||||||
|
* If both properties exist, it fetches the connected calendar credentials using the provided user credentials
|
||||||
|
* and checks for a matching external ID and integration from the list of connected calendars.
|
||||||
|
*
|
||||||
|
* If a match is found, it updates the `credentialId` with the one from the connected calendar.
|
||||||
|
* Otherwise, it throws an HTTP error with a 400 status indicating an invalid credential ID.
|
||||||
|
*
|
||||||
|
* If the parsed body does not contain the necessary properties, the function
|
||||||
|
* returns the `credentialId` from the destination calendar object.
|
||||||
|
*
|
||||||
|
* @param parsedBody - The parsed body from the incoming request, validated against a predefined schema.
|
||||||
|
* Checked if it contain properties like `integration` and `externalId`.
|
||||||
|
* @param userCredentials - An array of user credentials used to fetch the connected calendar credentials.
|
||||||
|
* @param destinationCalendarObject - An object representing the destination calendar. Primarily used
|
||||||
|
* to fetch the default `credentialId`.
|
||||||
|
*
|
||||||
|
* @returns - The verified `credentialId` either from the matched connected calendar in case of updating the destination calendar,
|
||||||
|
* or the provided destination calendar object in other cases.
|
||||||
|
*
|
||||||
|
* @throws HttpError - If no matching connected calendar is found for the given `integration` and `externalId`.
|
||||||
|
*/
|
||||||
|
async function verifyCredentialsAndGetId({
|
||||||
|
parsedBody,
|
||||||
|
userCredentials,
|
||||||
|
currentCredentialId,
|
||||||
|
}: {
|
||||||
|
parsedBody: z.infer<typeof schemaDestinationCalendarEditBodyParams>;
|
||||||
|
userCredentials: UserCredentialType[];
|
||||||
|
currentCredentialId: number | null;
|
||||||
|
}) {
|
||||||
|
if (parsedBody.integration && parsedBody.externalId) {
|
||||||
|
const calendarCredentials = getCalendarCredentials(userCredentials);
|
||||||
|
|
||||||
|
const { connectedCalendars } = await getConnectedCalendars(
|
||||||
|
calendarCredentials,
|
||||||
|
[],
|
||||||
|
parsedBody.externalId
|
||||||
|
);
|
||||||
|
const eligibleCalendars = connectedCalendars[0]?.calendars?.filter((calendar) => !calendar.readOnly);
|
||||||
|
const calendar = eligibleCalendars?.find(
|
||||||
|
(c) => c.externalId === parsedBody.externalId && c.integration === parsedBody.integration
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!calendar?.credentialId)
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: "Bad request, credential id invalid",
|
||||||
|
});
|
||||||
|
return calendar?.credentialId;
|
||||||
|
}
|
||||||
|
return currentCredentialId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the request for updating a destination calendar.
|
||||||
|
*
|
||||||
|
* This function checks the validity of the provided eventTypeId against the existing destination calendar object
|
||||||
|
* in the sense that if the destination calendar is not linked to an event type, the eventTypeId can not be provided.
|
||||||
|
*
|
||||||
|
* It also ensures that the eventTypeId, if provided, belongs to the assigned user.
|
||||||
|
*
|
||||||
|
* @param destinationCalendarObject - An object representing the destination calendar.
|
||||||
|
* @param parsedBody - The parsed body from the incoming request, validated against a predefined schema.
|
||||||
|
* @param assignedUserId - The user ID assigned for the operation, which might be an admin or a regular user.
|
||||||
|
* @param prisma - An instance of PrismaClient for database operations.
|
||||||
|
*
|
||||||
|
* @throws HttpError - If the validation fails or inconsistencies are detected in the request data.
|
||||||
|
*/
|
||||||
|
async function validateRequestAndOwnership({
|
||||||
|
destinationCalendarObject,
|
||||||
|
parsedBody,
|
||||||
|
assignedUserId,
|
||||||
|
prisma,
|
||||||
|
}: {
|
||||||
|
destinationCalendarObject: DestinationCalendarType;
|
||||||
|
parsedBody: z.infer<typeof schemaDestinationCalendarEditBodyParams>;
|
||||||
|
assignedUserId: number;
|
||||||
|
prisma: PrismaClient;
|
||||||
|
}) {
|
||||||
|
if (parsedBody.eventTypeId) {
|
||||||
|
if (!destinationCalendarObject.eventTypeId) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: `The provided destination calendar can not be linked to an event type`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userEventType = await prisma.eventType.findFirst({
|
||||||
|
where: { id: parsedBody.eventTypeId },
|
||||||
|
select: { userId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userEventType || userEventType.userId !== assignedUserId) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: `Event type with ID ${parsedBody.eventTypeId} not found`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsedBody.eventTypeId) {
|
||||||
|
if (destinationCalendarObject.eventTypeId) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: `The provided destination calendar can only be linked to an event type`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (destinationCalendarObject.userId !== assignedUserId) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 403,
|
||||||
|
message: `Forbidden`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the destination calendar based on the provided ID as the path parameter, specifically `credentialId` and `eventTypeId`.
|
||||||
|
*
|
||||||
|
* If no matching destination calendar is found for the provided ID, an HTTP error with a 404 status
|
||||||
|
* indicating that the desired destination calendar was not found is thrown.
|
||||||
|
*
|
||||||
|
* @param id - The ID of the destination calendar to be retrieved.
|
||||||
|
* @param prisma - An instance of PrismaClient for database operations.
|
||||||
|
*
|
||||||
|
* @returns - An object containing details of the matching destination calendar, specifically `credentialId` and `eventTypeId`.
|
||||||
|
*
|
||||||
|
* @throws HttpError - If no destination calendar matches the provided ID.
|
||||||
|
*/
|
||||||
|
async function getDestinationCalendar(id: number, prisma: PrismaClient) {
|
||||||
|
const destinationCalendarObject = await prisma.destinationCalendar.findFirst({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
select: { userId: true, eventTypeId: true, credentialId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!destinationCalendarObject) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: `Destination calendar with ID ${id} not found`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return destinationCalendarObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateIntegrationInput(parsedBody: z.infer<typeof schemaDestinationCalendarEditBodyParams>) {
|
||||||
|
if (parsedBody.integration && !parsedBody.externalId) {
|
||||||
|
throw new HttpError({ statusCode: 400, message: "External Id is required with integration value" });
|
||||||
|
}
|
||||||
|
if (!parsedBody.integration && parsedBody.externalId) {
|
||||||
|
throw new HttpError({ statusCode: 400, message: "Integration value is required with external ID" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultResponder(patchHandler);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||||
|
|
||||||
|
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||||
|
|
||||||
|
import authMiddleware from "./_auth-middleware";
|
||||||
|
|
||||||
|
export default withMiddleware()(
|
||||||
|
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
await authMiddleware(req);
|
||||||
|
return defaultHandler({
|
||||||
|
GET: import("./_get"),
|
||||||
|
PATCH: import("./_patch"),
|
||||||
|
DELETE: import("./_delete"),
|
||||||
|
})(req, res);
|
||||||
|
})
|
||||||
|
);
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
|
||||||
|
import { extractUserIdsFromQuery } from "~/lib/utils/extractUserIdsFromQuery";
|
||||||
|
import { schemaDestinationCalendarReadPublic } from "~/lib/validations/destination-calendar";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /destination-calendars:
|
||||||
|
* get:
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: apiKey
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Your API key
|
||||||
|
* summary: Find all destination calendars
|
||||||
|
* tags:
|
||||||
|
* - destination-calendars
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: OK
|
||||||
|
* 401:
|
||||||
|
* description: Authorization information is missing or invalid.
|
||||||
|
* 404:
|
||||||
|
* description: No destination calendars were found
|
||||||
|
*/
|
||||||
|
async function getHandler(req: NextApiRequest) {
|
||||||
|
const { userId, prisma } = req;
|
||||||
|
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
||||||
|
|
||||||
|
const userEventTypes = await prisma.eventType.findMany({
|
||||||
|
where: { userId: { in: userIds } },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const userEventTypeIds = userEventTypes.map((eventType) => eventType.id);
|
||||||
|
|
||||||
|
const allDestinationCalendars = await prisma.destinationCalendar.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [{ userId: { in: userIds } }, { eventTypeId: { in: userEventTypeIds } }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allDestinationCalendars.length === 0)
|
||||||
|
new HttpError({ statusCode: 404, message: "No destination calendars were found" });
|
||||||
|
|
||||||
|
return {
|
||||||
|
destinationCalendars: allDestinationCalendars.map((destinationCalendar) =>
|
||||||
|
schemaDestinationCalendarReadPublic.parse(destinationCalendar)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultResponder(getHandler);
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
|
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||||
|
|
||||||
|
import {
|
||||||
|
schemaDestinationCalendarReadPublic,
|
||||||
|
schemaDestinationCalendarCreateBodyParams,
|
||||||
|
} from "~/lib/validations/destination-calendar";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /destination-calendars:
|
||||||
|
* post:
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: apiKey
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Your API key
|
||||||
|
* summary: Creates a new destination calendar
|
||||||
|
* requestBody:
|
||||||
|
* description: Create a new destination calendar for your events
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - integration
|
||||||
|
* - externalId
|
||||||
|
* - credentialId
|
||||||
|
* properties:
|
||||||
|
* integration:
|
||||||
|
* type: string
|
||||||
|
* description: 'The integration'
|
||||||
|
* externalId:
|
||||||
|
* type: string
|
||||||
|
* description: 'The external ID of the integration'
|
||||||
|
* eventTypeId:
|
||||||
|
* type: integer
|
||||||
|
* description: 'The ID of the eventType it is associated with'
|
||||||
|
* bookingId:
|
||||||
|
* type: integer
|
||||||
|
* description: 'The booking ID it is associated with'
|
||||||
|
* userId:
|
||||||
|
* type: integer
|
||||||
|
* description: 'The user it is associated with'
|
||||||
|
* tags:
|
||||||
|
* - destination-calendars
|
||||||
|
* responses:
|
||||||
|
* 201:
|
||||||
|
* description: OK, destination calendar created
|
||||||
|
* 400:
|
||||||
|
* description: Bad request. DestinationCalendar body is invalid.
|
||||||
|
* 401:
|
||||||
|
* description: Authorization information is missing or invalid.
|
||||||
|
*/
|
||||||
|
async function postHandler(req: NextApiRequest) {
|
||||||
|
const { userId, isAdmin, prisma, body } = req;
|
||||||
|
const parsedBody = schemaDestinationCalendarCreateBodyParams.parse(body);
|
||||||
|
await checkPermissions(req, userId);
|
||||||
|
|
||||||
|
const assignedUserId = isAdmin && parsedBody.userId ? parsedBody.userId : userId;
|
||||||
|
|
||||||
|
/* Check if credentialId data matches the ownership and integration passed in */
|
||||||
|
const userCredentials = await prisma.credential.findMany({
|
||||||
|
where: {
|
||||||
|
type: parsedBody.integration,
|
||||||
|
userId: assignedUserId,
|
||||||
|
},
|
||||||
|
select: credentialForCalendarServiceSelect,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userCredentials.length === 0)
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: "Bad request, credential id invalid",
|
||||||
|
});
|
||||||
|
|
||||||
|
const calendarCredentials = getCalendarCredentials(userCredentials);
|
||||||
|
|
||||||
|
const { connectedCalendars } = await getConnectedCalendars(calendarCredentials, [], parsedBody.externalId);
|
||||||
|
|
||||||
|
const eligibleCalendars = connectedCalendars[0]?.calendars?.filter((calendar) => !calendar.readOnly);
|
||||||
|
const calendar = eligibleCalendars?.find(
|
||||||
|
(c) => c.externalId === parsedBody.externalId && c.integration === parsedBody.integration
|
||||||
|
);
|
||||||
|
if (!calendar?.credentialId)
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: "Bad request, credential id invalid",
|
||||||
|
});
|
||||||
|
const credentialId = calendar.credentialId;
|
||||||
|
|
||||||
|
if (parsedBody.eventTypeId) {
|
||||||
|
const eventType = await prisma.eventType.findFirst({
|
||||||
|
where: { id: parsedBody.eventTypeId, userId: parsedBody.userId },
|
||||||
|
});
|
||||||
|
if (!eventType)
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: "Bad request, eventTypeId invalid",
|
||||||
|
});
|
||||||
|
parsedBody.userId = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const destination_calendar = await prisma.destinationCalendar.create({
|
||||||
|
data: { ...parsedBody, credentialId },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
destinationCalendar: schemaDestinationCalendarReadPublic.parse(destination_calendar),
|
||||||
|
message: "Destination calendar created successfully",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkPermissions(req: NextApiRequest, userId: number) {
|
||||||
|
const { isAdmin } = req;
|
||||||
|
const body = schemaDestinationCalendarCreateBodyParams.parse(req.body);
|
||||||
|
|
||||||
|
/* Non-admin users can only create destination calendars for themselves */
|
||||||
|
if (!isAdmin && body.userId)
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: "ADMIN required for `userId`",
|
||||||
|
});
|
||||||
|
/* Admin users are required to pass in a userId */
|
||||||
|
if (isAdmin && !body.userId) throw new HttpError({ statusCode: 400, message: "`userId` required" });
|
||||||
|
/* User should only be able to create for their own destination calendars*/
|
||||||
|
if (!isAdmin && body.eventTypeId) {
|
||||||
|
const ownsEventType = await req.prisma.eventType.findFirst({ where: { id: body.eventTypeId, userId } });
|
||||||
|
if (!ownsEventType) throw new HttpError({ statusCode: 401, message: "Unauthorized" });
|
||||||
|
}
|
||||||
|
// TODO:: Add support for team event types with validation
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultResponder(postHandler);
|
||||||
@@ -1,114 +1,10 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import { defaultHandler } from "@calcom/lib/server";
|
||||||
|
|
||||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||||
import type { DestinationCalendarResponse, DestinationCalendarsResponse } from "~/lib/types";
|
|
||||||
import {
|
|
||||||
schemaDestinationCalendarCreateBodyParams,
|
|
||||||
schemaDestinationCalendarReadPublic,
|
|
||||||
} from "~/lib/validations/destination-calendar";
|
|
||||||
|
|
||||||
async function createOrlistAllDestinationCalendars(
|
export default withMiddleware()(
|
||||||
{ method, body, userId, prisma }: NextApiRequest,
|
defaultHandler({
|
||||||
res: NextApiResponse<DestinationCalendarsResponse | DestinationCalendarResponse>
|
GET: import("./_get"),
|
||||||
) {
|
POST: import("./_post"),
|
||||||
if (method === "GET") {
|
})
|
||||||
/**
|
);
|
||||||
* @swagger
|
|
||||||
* /destination-calendars:
|
|
||||||
* get:
|
|
||||||
* parameters:
|
|
||||||
* - in: query
|
|
||||||
* name: apiKey
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* description: Your API key
|
|
||||||
* summary: Find all destination calendars
|
|
||||||
* tags:
|
|
||||||
* - destination-calendars
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: OK
|
|
||||||
* 401:
|
|
||||||
* description: Authorization information is missing or invalid.
|
|
||||||
* 404:
|
|
||||||
* description: No destination calendars were found
|
|
||||||
*/
|
|
||||||
const data = await prisma.destinationCalendar.findMany({ where: { userId } });
|
|
||||||
const destination_calendars = data.map((destinationCalendar) =>
|
|
||||||
schemaDestinationCalendarReadPublic.parse(destinationCalendar)
|
|
||||||
);
|
|
||||||
if (data) res.status(200).json({ destination_calendars });
|
|
||||||
else
|
|
||||||
(error: Error) =>
|
|
||||||
res.status(404).json({
|
|
||||||
message: "No DestinationCalendars were found",
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
} else if (method === "POST") {
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /destination-calendars:
|
|
||||||
* post:
|
|
||||||
* parameters:
|
|
||||||
* - in: query
|
|
||||||
* name: apiKey
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* description: Your API key
|
|
||||||
* summary: Creates a new destination calendar
|
|
||||||
* requestBody:
|
|
||||||
* description: Create a new destination calendar for your events
|
|
||||||
* required: true
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* required:
|
|
||||||
* - integration
|
|
||||||
* - externalId
|
|
||||||
* properties:
|
|
||||||
* integration:
|
|
||||||
* type: string
|
|
||||||
* description: 'The integration'
|
|
||||||
* externalId:
|
|
||||||
* type: string
|
|
||||||
* description: 'The external ID of the integration'
|
|
||||||
* eventTypeId:
|
|
||||||
* type: integer
|
|
||||||
* description: 'The ID of the eventType it is associated with'
|
|
||||||
* bookingId:
|
|
||||||
* type: integer
|
|
||||||
* description: 'The booking ID it is associated with'
|
|
||||||
* tags:
|
|
||||||
* - destination-calendars
|
|
||||||
* responses:
|
|
||||||
* 201:
|
|
||||||
* description: OK, destination calendar created
|
|
||||||
* 400:
|
|
||||||
* description: Bad request. DestinationCalendar body is invalid.
|
|
||||||
* 401:
|
|
||||||
* description: Authorization information is missing or invalid.
|
|
||||||
*/
|
|
||||||
const safe = schemaDestinationCalendarCreateBodyParams.safeParse(body);
|
|
||||||
if (!safe.success) {
|
|
||||||
res.status(400).json({ message: "Invalid request body" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await prisma.destinationCalendar.create({ data: { ...safe.data, userId } });
|
|
||||||
const destination_calendar = schemaDestinationCalendarReadPublic.parse(data);
|
|
||||||
|
|
||||||
if (destination_calendar)
|
|
||||||
res.status(201).json({ destination_calendar, message: "DestinationCalendar created successfully" });
|
|
||||||
else
|
|
||||||
(error: Error) =>
|
|
||||||
res.status(400).json({
|
|
||||||
message: "Could not create new destinationCalendar",
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
} else res.status(405).json({ message: `Method ${method} not allowed` });
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withMiddleware("HTTP_GET_OR_POST")(createOrlistAllDestinationCalendars);
|
|
||||||
@@ -2,6 +2,7 @@ import type { NextApiRequest } from "next";
|
|||||||
|
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
import { MembershipRole } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||||
@@ -51,6 +52,7 @@ export async function getHandler(req: NextApiRequest) {
|
|||||||
customInputs: true,
|
customInputs: true,
|
||||||
team: { select: { slug: true } },
|
team: { select: { slug: true } },
|
||||||
users: true,
|
users: true,
|
||||||
|
hosts: { select: { userId: true, isFixed: true } },
|
||||||
owner: { select: { username: true, id: true } },
|
owner: { select: { username: true, id: true } },
|
||||||
children: { select: { id: true, userId: true } },
|
children: { select: { id: true, userId: true } },
|
||||||
},
|
},
|
||||||
@@ -88,7 +90,9 @@ async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
|
|||||||
if (req.isAdmin) return true;
|
if (req.isAdmin) return true;
|
||||||
if (eventType?.teamId) {
|
if (eventType?.teamId) {
|
||||||
req.query.teamId = String(eventType.teamId);
|
req.query.teamId = String(eventType.teamId);
|
||||||
await canAccessTeamEventOrThrow(req, "MEMBER");
|
await canAccessTeamEventOrThrow(req, {
|
||||||
|
in: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (eventType?.userId === req.userId) return true; // is owner.
|
if (eventType?.userId === req.userId) return true; // is owner.
|
||||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ async function getHandler(req: NextApiRequest) {
|
|||||||
customInputs: true,
|
customInputs: true,
|
||||||
team: { select: { slug: true } },
|
team: { select: { slug: true } },
|
||||||
users: true,
|
users: true,
|
||||||
|
hosts: { select: { userId: true, isFixed: true } },
|
||||||
owner: { select: { username: true, id: true } },
|
owner: { select: { username: true, id: true } },
|
||||||
children: { select: { id: true, userId: true } },
|
children: { select: { id: true, userId: true } },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import type { NextApiRequest } from "next";
|
|||||||
|
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
import { MembershipRole } from "@calcom/prisma/client";
|
||||||
|
|
||||||
import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||||
|
import { canUserAccessTeamWithRole } from "~/pages/api/teams/[teamId]/_auth-middleware";
|
||||||
|
|
||||||
import checkParentEventOwnership from "./_utils/checkParentEventOwnership";
|
import checkParentEventOwnership from "./_utils/checkParentEventOwnership";
|
||||||
import checkTeamEventEditPermission from "./_utils/checkTeamEventEditPermission";
|
import checkTeamEventEditPermission from "./_utils/checkTeamEventEditPermission";
|
||||||
@@ -284,8 +286,8 @@ async function postHandler(req: NextApiRequest) {
|
|||||||
await checkPermissions(req);
|
await checkPermissions(req);
|
||||||
|
|
||||||
if (parsedBody.parentId) {
|
if (parsedBody.parentId) {
|
||||||
await checkParentEventOwnership(parsedBody.parentId, userId);
|
await checkParentEventOwnership(req);
|
||||||
await checkUserMembership(parsedBody.parentId, parsedBody.userId);
|
await checkUserMembership(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAdmin && parsedBody.userId) {
|
if (isAdmin && parsedBody.userId) {
|
||||||
@@ -299,7 +301,7 @@ async function postHandler(req: NextApiRequest) {
|
|||||||
data.hosts = { createMany: { data: hosts } };
|
data.hosts = { createMany: { data: hosts } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventType = await prisma.eventType.create({ data });
|
const eventType = await prisma.eventType.create({ data, include: { hosts: true } });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
event_type: schemaEventTypeReadPublic.parse(eventType),
|
event_type: schemaEventTypeReadPublic.parse(eventType),
|
||||||
@@ -316,8 +318,20 @@ async function checkPermissions(req: NextApiRequest) {
|
|||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
message: "ADMIN required for `userId`",
|
message: "ADMIN required for `userId`",
|
||||||
});
|
});
|
||||||
/* Admin users are required to pass in a userId */
|
if (
|
||||||
if (isAdmin && !body.userId) throw new HttpError({ statusCode: 400, message: "`userId` required" });
|
body.teamId &&
|
||||||
|
!isAdmin &&
|
||||||
|
!(await canUserAccessTeamWithRole(req.prisma, req.userId, isAdmin, body.teamId, {
|
||||||
|
in: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: "ADMIN required for `teamId`",
|
||||||
|
});
|
||||||
|
/* Admin users are required to pass in a userId or teamId */
|
||||||
|
if (isAdmin && !body.userId && !body.teamId)
|
||||||
|
throw new HttpError({ statusCode: 400, message: "`userId` or `teamId` required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defaultResponder(postHandler);
|
export default defaultResponder(postHandler);
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a user, identified by the provided userId, has ownership (or admin rights) over
|
* Checks if a user, identified by the provided userId, has ownership (or admin rights) over
|
||||||
* the team associated with the event type identified by the parentId.
|
* the team associated with the event type identified by the parentId.
|
||||||
*
|
*
|
||||||
* @param parentId - The ID of the parent event type.
|
* @param req - The current request
|
||||||
* @param userId - The ID of the user.
|
|
||||||
*
|
*
|
||||||
* @throws {HttpError} If the parent event type is not found,
|
* @throws {HttpError} If the parent event type is not found,
|
||||||
* if the parent event type doesn't belong to any team,
|
* if the parent event type doesn't belong to any team,
|
||||||
* or if the user doesn't have ownership or admin rights to the associated team.
|
* or if the user doesn't have ownership or admin rights to the associated team.
|
||||||
*/
|
*/
|
||||||
export default async function checkParentEventOwnership(parentId: number, userId: number) {
|
export default async function checkParentEventOwnership(req: NextApiRequest) {
|
||||||
|
const { userId, prisma, body } = req;
|
||||||
|
/** These are already parsed upstream, we can assume they're good here. */
|
||||||
|
const parentId = Number(body.parentId);
|
||||||
const parentEventType = await prisma.eventType.findUnique({
|
const parentEventType = await prisma.eventType.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: parentId,
|
id: parentId,
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a user, identified by the provided userId, is a member of the team associated
|
* Checks if a user, identified by the provided userId, is a member of the team associated
|
||||||
* with the event type identified by the parentId.
|
* with the event type identified by the parentId.
|
||||||
*
|
*
|
||||||
* @param parentId - The ID of the event type.
|
* @param req - The current request
|
||||||
* @param userId - The ID of the user.
|
|
||||||
*
|
*
|
||||||
* @throws {HttpError} If the event type is not found,
|
* @throws {HttpError} If the event type is not found,
|
||||||
* if the event type doesn't belong to any team,
|
* if the event type doesn't belong to any team,
|
||||||
* or if the user isn't a member of the associated team.
|
* or if the user isn't a member of the associated team.
|
||||||
*/
|
*/
|
||||||
export default async function checkUserMembership(parentId: number, userId?: number) {
|
export default async function checkUserMembership(req: NextApiRequest) {
|
||||||
|
const { prisma, body } = req;
|
||||||
|
/** These are already parsed upstream, we can assume they're good here. */
|
||||||
|
const parentId = Number(body.parentId);
|
||||||
|
const userId = Number(body.userId);
|
||||||
const parentEventType = await prisma.eventType.findUnique({
|
const parentEventType = await prisma.eventType.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: parentId,
|
id: parentId,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
export default async function CalcomApi(_: NextApiRequest, res: NextApiResponse) {
|
export default async function CalcomApi(_: NextApiRequest, res: NextApiResponse) {
|
||||||
res.status(201).json({ message: "Welcome to Cal.com API - docs are at https://developer.cal.com/api" });
|
res.status(200).json({ message: "Welcome to Cal.com API - docs are at https://developer.cal.com/api" });
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,47 @@
|
|||||||
|
import timezone from "dayjs/plugin/timezone";
|
||||||
|
import utc from "dayjs/plugin/utc";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { isSupportedTimeZone } from "@calcom/lib/date-fns";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
import { createContext } from "@calcom/trpc/server/createContext";
|
import { createContext } from "@calcom/trpc/server/createContext";
|
||||||
import { slotsRouter } from "@calcom/trpc/server/routers/viewer/slots/_router";
|
import { getScheduleSchema } from "@calcom/trpc/server/routers/viewer/slots/types";
|
||||||
|
import { getAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util";
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
|
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
|
||||||
|
|
||||||
|
// Apply plugins
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
/** @see https://trpc.io/docs/server-side-calls */
|
|
||||||
const ctx = await createContext({ req, res });
|
|
||||||
const caller = slotsRouter.createCaller(ctx);
|
|
||||||
try {
|
try {
|
||||||
|
const { usernameList, ...rest } = req.query;
|
||||||
|
let slugs = usernameList;
|
||||||
|
if (!Array.isArray(usernameList)) {
|
||||||
|
slugs = usernameList ? [usernameList] : [];
|
||||||
|
}
|
||||||
|
const input = getScheduleSchema.parse({ usernameList: slugs, ...rest });
|
||||||
|
const timeZoneSupported = input.timeZone ? isSupportedTimeZone(input.timeZone) : false;
|
||||||
|
const availableSlots = await getAvailableSlots({ ctx: await createContext({ req, res }), input });
|
||||||
|
const slotsInProvidedTimeZone = timeZoneSupported
|
||||||
|
? Object.keys(availableSlots.slots).reduce(
|
||||||
|
(acc: Record<string, { time: string; attendees?: number; bookingUid?: string }[]>, date) => {
|
||||||
|
acc[date] = availableSlots.slots[date].map((slot) => ({
|
||||||
|
...slot,
|
||||||
|
time: dayjs(slot.time).tz(input.timeZone).format(),
|
||||||
|
}));
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
: availableSlots;
|
||||||
|
|
||||||
|
return slotsInProvidedTimeZone;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
return await caller.getSchedule(req.query as any /* Let tRPC handle this */);
|
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
if (cause instanceof TRPCError) {
|
if (cause instanceof TRPCError) {
|
||||||
const statusCode = getHTTPStatusCodeFromError(cause);
|
const statusCode = getHTTPStatusCodeFromError(cause);
|
||||||
|
|||||||
@@ -22,7 +22,21 @@ export async function checkPermissions(
|
|||||||
role: Prisma.MembershipWhereInput["role"] = MembershipRole.OWNER
|
role: Prisma.MembershipWhereInput["role"] = MembershipRole.OWNER
|
||||||
) {
|
) {
|
||||||
const { userId, prisma, isAdmin } = req;
|
const { userId, prisma, isAdmin } = req;
|
||||||
const { teamId } = schemaQueryTeamId.parse(req.query);
|
const { teamId } = schemaQueryTeamId.parse({
|
||||||
|
teamId: req.query.teamId,
|
||||||
|
version: req.query.version,
|
||||||
|
apiKey: req.query.apiKey,
|
||||||
|
});
|
||||||
|
return canUserAccessTeamWithRole(prisma, userId, isAdmin, teamId, role);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function canUserAccessTeamWithRole(
|
||||||
|
prisma: NextApiRequest["prisma"],
|
||||||
|
userId: number,
|
||||||
|
isAdmin: boolean,
|
||||||
|
teamId: number,
|
||||||
|
role: Prisma.MembershipWhereInput["role"] = MembershipRole.OWNER
|
||||||
|
) {
|
||||||
const args: Prisma.TeamFindFirstArgs = { where: { id: teamId } };
|
const args: Prisma.TeamFindFirstArgs = { where: { id: teamId } };
|
||||||
/** If not ADMIN then we check if the actual user belongs to team and matches the required role */
|
/** If not ADMIN then we check if the actual user belongs to team and matches the required role */
|
||||||
if (!isAdmin) args.where = { ...args.where, members: { some: { userId, role } } };
|
if (!isAdmin) args.where = { ...args.where, members: { some: { userId, role } } };
|
||||||
|
|||||||
@@ -64,6 +64,25 @@ export async function patchHandler(req: NextApiRequest) {
|
|||||||
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" });
|
||||||
|
|
||||||
|
// Check if parentId is related to this user
|
||||||
|
if (data.parentId && data.parentId === teamId) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: "Bad request: Parent id cannot be the same as the team id.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (data.parentId) {
|
||||||
|
const parentTeam = await prisma.team.findFirst({
|
||||||
|
where: { id: data.parentId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
|
||||||
|
});
|
||||||
|
if (!parentTeam)
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: "Unauthorized: Invalid parent id. You can only use parent id of your own teams.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let paymentUrl;
|
let paymentUrl;
|
||||||
if (_team.slug === null && data.slug) {
|
if (_team.slug === null && data.slug) {
|
||||||
data.metadata = {
|
data.metadata = {
|
||||||
|
|||||||
@@ -56,6 +56,14 @@ async function getHandler(req: NextApiRequest) {
|
|||||||
members: { some: { userId } },
|
members: { some: { userId } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
customInputs: true,
|
||||||
|
team: { select: { slug: true } },
|
||||||
|
users: true,
|
||||||
|
hosts: { select: { userId: true, isFixed: true } },
|
||||||
|
owner: { select: { username: true, id: true } },
|
||||||
|
children: { select: { id: true, userId: true } },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const data = await prisma.eventType.findMany(args);
|
const data = await prisma.eventType.findMany(args);
|
||||||
|
|||||||
@@ -68,6 +68,18 @@ async function postHandler(req: NextApiRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if parentId is related to this user
|
||||||
|
if (data.parentId) {
|
||||||
|
const parentTeam = await prisma.team.findFirst({
|
||||||
|
where: { id: data.parentId, members: { some: { userId, role: { in: ["OWNER", "ADMIN"] } } } },
|
||||||
|
});
|
||||||
|
if (!parentTeam)
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: "Unauthorized: Invalid parent id. You can only use parent id of your own teams.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Perhaps there is a better fix for this?
|
// TODO: Perhaps there is a better fix for this?
|
||||||
const cloneData: typeof data & {
|
const cloneData: typeof data & {
|
||||||
metadata: NonNullable<typeof data.metadata> | undefined;
|
metadata: NonNullable<typeof data.metadata> | undefined;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
|
import { deleteUser } from "@calcom/features/users/lib/userDeletionService";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
|
||||||
@@ -41,10 +42,18 @@ export async function deleteHandler(req: NextApiRequest) {
|
|||||||
// Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user
|
// Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user
|
||||||
if (!isAdmin && query.userId !== req.userId) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
if (!isAdmin && query.userId !== req.userId) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({ where: { id: query.userId } });
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: query.userId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
metadata: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
if (!user) throw new HttpError({ statusCode: 404, message: "User not found" });
|
if (!user) throw new HttpError({ statusCode: 404, message: "User not found" });
|
||||||
|
|
||||||
await prisma.user.delete({ where: { id: user.id } });
|
await deleteUser(user);
|
||||||
|
|
||||||
return { message: `User with id: ${user.id} deleted successfully` };
|
return { message: `User with id: ${user.id} deleted successfully` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ import { schemaUserEditBodyParams, schemaUserReadPublic } from "~/lib/validation
|
|||||||
* timeZone:
|
* timeZone:
|
||||||
* description: The user's time zone
|
* description: The user's time zone
|
||||||
* type: string
|
* type: string
|
||||||
|
* hideBranding:
|
||||||
|
* description: Remove branding from the user's calendar page
|
||||||
|
* type: boolean
|
||||||
* theme:
|
* theme:
|
||||||
* description: Default theme for the user. Acceptable values are one of [DARK, LIGHT]
|
* description: Default theme for the user. Acceptable values are one of [DARK, LIGHT]
|
||||||
* type: string
|
* type: string
|
||||||
@@ -62,6 +65,9 @@ import { schemaUserEditBodyParams, schemaUserReadPublic } from "~/lib/validation
|
|||||||
* locale:
|
* locale:
|
||||||
* description: The user's locale. Acceptable values are one of [EN, FR, IT, RU, ES, DE, PT, RO, NL, PT_BR, ES_419, KO, JA, PL, AR, IW, ZH_CH, ZH_TW, CS, SR, SV, VI]
|
* description: The user's locale. Acceptable values are one of [EN, FR, IT, RU, ES, DE, PT, RO, NL, PT_BR, ES_419, KO, JA, PL, AR, IW, ZH_CH, ZH_TW, CS, SR, SV, VI]
|
||||||
* type: string
|
* type: string
|
||||||
|
* avatar:
|
||||||
|
* description: The user's avatar, in base64 format
|
||||||
|
* type: string
|
||||||
* examples:
|
* examples:
|
||||||
* user:
|
* user:
|
||||||
* summary: An example of USER
|
* summary: An example of USER
|
||||||
@@ -79,7 +85,7 @@ import { schemaUserEditBodyParams, schemaUserReadPublic } from "~/lib/validation
|
|||||||
* - users
|
* - users
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: OK, user edited successfuly
|
* description: OK, user edited successfully
|
||||||
* 400:
|
* 400:
|
||||||
* description: Bad request. User body is invalid.
|
* description: Bad request. User body is invalid.
|
||||||
* 401:
|
* 401:
|
||||||
@@ -94,9 +100,10 @@ export async function patchHandler(req: NextApiRequest) {
|
|||||||
if (!isAdmin && query.userId !== req.userId) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
if (!isAdmin && query.userId !== req.userId) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||||
|
|
||||||
const body = await schemaUserEditBodyParams.parseAsync(req.body);
|
const body = await schemaUserEditBodyParams.parseAsync(req.body);
|
||||||
// disable role changes unless admin.
|
// disable role or branding changes unless admin.
|
||||||
if (!isAdmin && body.role) {
|
if (!isAdmin) {
|
||||||
body.role = undefined;
|
if (body.role) body.role = undefined;
|
||||||
|
if (body.hideBranding) body.hideBranding = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userSchedules = await prisma.schedule.findMany({
|
const userSchedules = await prisma.schedule.findMany({
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ import { schemaUserCreateBodyParams } from "~/lib/validations/user";
|
|||||||
* darkBrandColor:
|
* darkBrandColor:
|
||||||
* description: The new user's brand color for dark mode
|
* description: The new user's brand color for dark mode
|
||||||
* type: string
|
* type: string
|
||||||
|
* hideBranding:
|
||||||
|
* description: Remove branding from the user's calendar page
|
||||||
|
* type: boolean
|
||||||
* weekStart:
|
* weekStart:
|
||||||
* description: Start of the week. Acceptable values are one of [SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY]
|
* description: Start of the week. Acceptable values are one of [SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY]
|
||||||
* type: string
|
* type: string
|
||||||
@@ -57,6 +60,9 @@ import { schemaUserCreateBodyParams } from "~/lib/validations/user";
|
|||||||
* locale:
|
* locale:
|
||||||
* description: The new user's locale. Acceptable values are one of [EN, FR, IT, RU, ES, DE, PT, RO, NL, PT_BR, ES_419, KO, JA, PL, AR, IW, ZH_CH, ZH_TW, CS, SR, SV, VI]
|
* description: The new user's locale. Acceptable values are one of [EN, FR, IT, RU, ES, DE, PT, RO, NL, PT_BR, ES_419, KO, JA, PL, AR, IW, ZH_CH, ZH_TW, CS, SR, SV, VI]
|
||||||
* type: string
|
* type: string
|
||||||
|
* avatar:
|
||||||
|
* description: The user's avatar, in base64 format
|
||||||
|
* type: string
|
||||||
* examples:
|
* examples:
|
||||||
* user:
|
* user:
|
||||||
* summary: An example of USER
|
* summary: An example of USER
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ import { schemaWebhookEditBodyParams, schemaWebhookReadPublic } from "~/lib/vali
|
|||||||
* eventTypeId:
|
* eventTypeId:
|
||||||
* type: number
|
* type: number
|
||||||
* description: The event type ID if this webhook should be associated with only that event type
|
* description: The event type ID if this webhook should be associated with only that event type
|
||||||
|
* secret:
|
||||||
|
* type: string
|
||||||
|
* description: The secret to verify the authenticity of the received payload
|
||||||
* tags:
|
* tags:
|
||||||
* - webhooks
|
* - webhooks
|
||||||
* externalDocs:
|
* externalDocs:
|
||||||
@@ -66,7 +69,12 @@ import { schemaWebhookEditBodyParams, schemaWebhookReadPublic } from "~/lib/vali
|
|||||||
export async function patchHandler(req: NextApiRequest) {
|
export async function patchHandler(req: NextApiRequest) {
|
||||||
const { prisma, query, userId, isAdmin } = req;
|
const { prisma, query, userId, isAdmin } = req;
|
||||||
const { id } = schemaQueryIdAsString.parse(query);
|
const { id } = schemaQueryIdAsString.parse(query);
|
||||||
const { eventTypeId, userId: bodyUserId, ...data } = schemaWebhookEditBodyParams.parse(req.body);
|
const {
|
||||||
|
eventTypeId,
|
||||||
|
userId: bodyUserId,
|
||||||
|
eventTriggers,
|
||||||
|
...data
|
||||||
|
} = schemaWebhookEditBodyParams.parse(req.body);
|
||||||
const args: Prisma.WebhookUpdateArgs = { where: { id }, data };
|
const args: Prisma.WebhookUpdateArgs = { where: { id }, data };
|
||||||
|
|
||||||
if (eventTypeId) {
|
if (eventTypeId) {
|
||||||
@@ -84,6 +92,11 @@ export async function patchHandler(req: NextApiRequest) {
|
|||||||
args.data.userId = bodyUserId;
|
args.data.userId = bodyUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.data.eventTriggers) {
|
||||||
|
const eventTriggersSet = new Set(eventTriggers);
|
||||||
|
args.data.eventTriggers = Array.from(eventTriggersSet);
|
||||||
|
}
|
||||||
|
|
||||||
const result = await prisma.webhook.update(args);
|
const result = await prisma.webhook.update(args);
|
||||||
return { webhook: schemaWebhookReadPublic.parse(result) };
|
return { webhook: schemaWebhookReadPublic.parse(result) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ import { schemaWebhookCreateBodyParams, schemaWebhookReadPublic } from "~/lib/va
|
|||||||
* eventTypeId:
|
* eventTypeId:
|
||||||
* type: number
|
* type: number
|
||||||
* description: The event type ID if this webhook should be associated with only that event type
|
* description: The event type ID if this webhook should be associated with only that event type
|
||||||
|
* secret:
|
||||||
|
* type: string
|
||||||
|
* description: The secret to verify the authenticity of the received payload
|
||||||
* tags:
|
* tags:
|
||||||
* - webhooks
|
* - webhooks
|
||||||
* externalDocs:
|
* externalDocs:
|
||||||
@@ -63,7 +66,12 @@ import { schemaWebhookCreateBodyParams, schemaWebhookReadPublic } from "~/lib/va
|
|||||||
*/
|
*/
|
||||||
async function postHandler(req: NextApiRequest) {
|
async function postHandler(req: NextApiRequest) {
|
||||||
const { userId, isAdmin, prisma } = req;
|
const { userId, isAdmin, prisma } = req;
|
||||||
const { eventTypeId, userId: bodyUserId, ...body } = schemaWebhookCreateBodyParams.parse(req.body);
|
const {
|
||||||
|
eventTypeId,
|
||||||
|
userId: bodyUserId,
|
||||||
|
eventTriggers,
|
||||||
|
...body
|
||||||
|
} = schemaWebhookCreateBodyParams.parse(req.body);
|
||||||
const args: Prisma.WebhookCreateArgs = { data: { id: uuidv4(), ...body } };
|
const args: Prisma.WebhookCreateArgs = { data: { id: uuidv4(), ...body } };
|
||||||
|
|
||||||
// If no event type, we assume is for the current user. If admin we run more checks below...
|
// If no event type, we assume is for the current user. If admin we run more checks below...
|
||||||
@@ -84,6 +92,11 @@ async function postHandler(req: NextApiRequest) {
|
|||||||
args.data.userId = bodyUserId;
|
args.data.userId = bodyUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.data.eventTriggers) {
|
||||||
|
const eventTriggersSet = new Set(eventTriggers);
|
||||||
|
args.data.eventTriggers = Array.from(eventTriggersSet);
|
||||||
|
}
|
||||||
|
|
||||||
const data = await prisma.webhook.create(args);
|
const data = await prisma.webhook.create(args);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
tracesSampleRate: 1.0,
|
||||||
|
});
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// TODO: Fix tests (These test were never running due to the vitest workspace config)
|
||||||
import prismaMock from "../../../../../tests/libs/__mocks__/prisma";
|
import prismaMock from "../../../../../tests/libs/__mocks__/prisma";
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
@@ -7,6 +8,7 @@ import { describe, expect, test, vi } from "vitest";
|
|||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||||
|
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||||
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
|
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
@@ -21,7 +23,7 @@ vi.mock("@calcom/lib/server/i18n", () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("POST /api/bookings", () => {
|
describe.skipIf(true)("POST /api/bookings", () => {
|
||||||
describe("Errors", () => {
|
describe("Errors", () => {
|
||||||
test("Missing required data", async () => {
|
test("Missing required data", async () => {
|
||||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
@@ -31,7 +33,7 @@ describe("POST /api/bookings", () => {
|
|||||||
|
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
|
|
||||||
expect(res._getStatusCode()).toBe(400);
|
expect(res.statusCode).toBe(400);
|
||||||
expect(JSON.parse(res._getData())).toEqual(
|
expect(JSON.parse(res._getData())).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
message:
|
message:
|
||||||
@@ -147,7 +149,7 @@ describe("POST /api/bookings", () => {
|
|||||||
expect(res._getStatusCode()).toBe(500);
|
expect(res._getStatusCode()).toBe(500);
|
||||||
expect(JSON.parse(res._getData())).toEqual(
|
expect(JSON.parse(res._getData())).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
message: "No available users found.",
|
message: ErrorCode.NoAvailableUsersFound,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { Request, Response } from "express";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { createMocks } from "node-mocks-http";
|
||||||
|
import { describe, vi, it, expect, afterEach } from "vitest";
|
||||||
|
|
||||||
|
import { addRequestId } from "../../../lib/helpers/addRequestid";
|
||||||
|
|
||||||
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Adds a request ID", () => {
|
||||||
|
it("Should attach a request ID to the request", async () => {
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "POST",
|
||||||
|
body: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const middleware = {
|
||||||
|
fn: addRequestId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const serverNext = vi.fn((next: void) => Promise.resolve(next));
|
||||||
|
|
||||||
|
const middlewareSpy = vi.spyOn(middleware, "fn");
|
||||||
|
|
||||||
|
await middleware.fn(req, res, serverNext);
|
||||||
|
|
||||||
|
expect(middlewareSpy).toBeCalled();
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res.getHeader("Calcom-Response-ID")).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import type { Request, Response } from "express";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { createMocks } from "node-mocks-http";
|
||||||
|
import { describe, vi, it, expect, afterEach } from "vitest";
|
||||||
|
|
||||||
|
import { httpMethod } from "../../../lib/helpers/httpMethods";
|
||||||
|
|
||||||
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("HTTP Methods function only allows the correct HTTP Methods", () => {
|
||||||
|
it("Should allow the passed in Method", async () => {
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "POST",
|
||||||
|
body: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const middleware = {
|
||||||
|
fn: httpMethod("POST"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const serverNext = vi.fn((next: void) => Promise.resolve(next));
|
||||||
|
|
||||||
|
const middlewareSpy = vi.spyOn(middleware, "fn");
|
||||||
|
|
||||||
|
await middleware.fn(req, res, serverNext);
|
||||||
|
|
||||||
|
expect(middlewareSpy).toBeCalled();
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
});
|
||||||
|
it("Should allow the passed in Method", async () => {
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "POST",
|
||||||
|
body: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const middleware = {
|
||||||
|
fn: httpMethod("GET"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const serverNext = vi.fn((next: void) => Promise.resolve(next));
|
||||||
|
const middlewareSpy = vi.spyOn(middleware, "fn");
|
||||||
|
|
||||||
|
await middleware.fn(req, res, serverNext);
|
||||||
|
|
||||||
|
expect(middlewareSpy).toBeCalled();
|
||||||
|
expect(res.statusCode).toBe(405);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import type { Request, Response } from "express";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { createMocks } from "node-mocks-http";
|
||||||
|
import { describe, vi, it, expect, afterEach } from "vitest";
|
||||||
|
|
||||||
|
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
|
||||||
|
|
||||||
|
import { isAdminGuard } from "~/lib/utils/isAdmin";
|
||||||
|
|
||||||
|
import { verifyApiKey } from "../../../lib/helpers/verifyApiKey";
|
||||||
|
|
||||||
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@calcom/features/ee/common/server/checkLicense", () => {
|
||||||
|
return {
|
||||||
|
default: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("~/lib/utils/isAdmin", () => {
|
||||||
|
return {
|
||||||
|
isAdminGuard: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Verify API key", () => {
|
||||||
|
it("It should throw an error if the api key is not valid", async () => {
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "POST",
|
||||||
|
body: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const middleware = {
|
||||||
|
fn: verifyApiKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(checkLicense).mockResolvedValue(false);
|
||||||
|
vi.mocked(isAdminGuard).mockResolvedValue(false);
|
||||||
|
|
||||||
|
const serverNext = vi.fn((next: void) => Promise.resolve(next));
|
||||||
|
|
||||||
|
const middlewareSpy = vi.spyOn(middleware, "fn");
|
||||||
|
|
||||||
|
await middleware.fn(req, res, serverNext);
|
||||||
|
|
||||||
|
expect(middlewareSpy).toBeCalled();
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
it("It should thow an error if no api key is provided", async () => {
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "POST",
|
||||||
|
body: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const middleware = {
|
||||||
|
fn: verifyApiKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(checkLicense).mockResolvedValue(true);
|
||||||
|
vi.mocked(isAdminGuard).mockResolvedValue(false);
|
||||||
|
|
||||||
|
const serverNext = vi.fn((next: void) => Promise.resolve(next));
|
||||||
|
|
||||||
|
const middlewareSpy = vi.spyOn(middleware, "fn");
|
||||||
|
|
||||||
|
await middleware.fn(req, res, serverNext);
|
||||||
|
|
||||||
|
expect(middlewareSpy).toBeCalled();
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { describe, vi, it, expect, afterEach } from "vitest";
|
||||||
|
|
||||||
|
import { middlewareOrder } from "../../../lib/helpers/withMiddleware";
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Not sure if there is much point testing this order is actually applied via an integration test:
|
||||||
|
// It is tested internally https://github.com/htunnicliff/next-api-middleware/blob/368b12aa30e79f4bd7cfe7aacc18da263cc3de2f/lib/label.spec.ts#L62
|
||||||
|
describe("API - withMiddleware test", () => {
|
||||||
|
it("Custom prisma should be before verifyApiKey", async () => {
|
||||||
|
const customPrismaClientIndex = middlewareOrder.indexOf("customPrismaClient");
|
||||||
|
const verifyApiKeyIndex = middlewareOrder.indexOf("verifyApiKey");
|
||||||
|
expect(customPrismaClientIndex).toBeLessThan(verifyApiKeyIndex);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"functions": {
|
||||||
|
"pages/api/slots/*.ts": {
|
||||||
|
"memory": 512
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Hello World
|
||||||
@@ -8,7 +8,9 @@ pnpm-debug.log*
|
|||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
storybook-static
|
storybook-static/*
|
||||||
|
!storybook-static/favicon.ico
|
||||||
|
!storybook-static/sb-cover.jpg
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
stories: [
|
|
||||||
"../intro.stories.mdx",
|
|
||||||
"../../../packages/ui/components/**/*.stories.mdx",
|
|
||||||
"../../../packages/atoms/**/*.stories.mdx",
|
|
||||||
"../../../packages/features/**/*.stories.mdx",
|
|
||||||
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
|
||||||
],
|
|
||||||
addons: [
|
|
||||||
"@storybook/addon-links",
|
|
||||||
"@storybook/addon-essentials",
|
|
||||||
"@storybook/addon-interactions",
|
|
||||||
"storybook-addon-rtl-direction",
|
|
||||||
"storybook-react-i18next",
|
|
||||||
"storybook-addon-next",
|
|
||||||
"storybook-addon-next-router",
|
|
||||||
/*{
|
|
||||||
name: "storybook-addon-next",
|
|
||||||
options: {
|
|
||||||
nextConfigPath: path.resolve(__dirname, "../../web/next.config.js"),
|
|
||||||
},
|
|
||||||
},*/
|
|
||||||
],
|
|
||||||
framework: "@storybook/react",
|
|
||||||
core: {
|
|
||||||
builder: "webpack5",
|
|
||||||
},
|
|
||||||
staticDirs: ["../public"],
|
|
||||||
webpackFinal: async (config, { configType }) => {
|
|
||||||
config.resolve.fallback = {
|
|
||||||
fs: false,
|
|
||||||
assert: false,
|
|
||||||
buffer: false,
|
|
||||||
console: false,
|
|
||||||
constants: false,
|
|
||||||
crypto: false,
|
|
||||||
domain: false,
|
|
||||||
events: false,
|
|
||||||
http: false,
|
|
||||||
https: false,
|
|
||||||
os: false,
|
|
||||||
path: false,
|
|
||||||
punycode: false,
|
|
||||||
process: false,
|
|
||||||
querystring: false,
|
|
||||||
stream: false,
|
|
||||||
string_decoder: false,
|
|
||||||
sys: false,
|
|
||||||
timers: false,
|
|
||||||
tty: false,
|
|
||||||
url: false,
|
|
||||||
util: false,
|
|
||||||
vm: false,
|
|
||||||
zlib: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
config.module.rules.push({
|
|
||||||
test: /\.css$/,
|
|
||||||
use: [
|
|
||||||
"style-loader",
|
|
||||||
{
|
|
||||||
loader: "css-loader",
|
|
||||||
options: {
|
|
||||||
modules: true, // Enable modules to help you using className
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
include: path.resolve(__dirname, "../src"),
|
|
||||||
});
|
|
||||||
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
typescript: { reactDocgen: "react-docgen" },
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import type { StorybookConfig } from "@storybook/nextjs";
|
||||||
|
import path, { dirname, join } from "path";
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: [
|
||||||
|
"../intro.stories.mdx",
|
||||||
|
"../../../packages/ui/components/**/*.stories.mdx", // legacy SB6 stories
|
||||||
|
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
||||||
|
"../../../packages/ui/components/**/*.docs.mdx",
|
||||||
|
"../../../packages/features/**/*.stories.@(js|jsx|ts|tsx)",
|
||||||
|
"../../../packages/features/**/*.docs.mdx",
|
||||||
|
"../../../packages/atoms/**/*.stories.@(js|jsx|ts|tsx)",
|
||||||
|
"../../../packages/atoms/**/*.docs.mdx",
|
||||||
|
],
|
||||||
|
|
||||||
|
addons: [
|
||||||
|
getAbsolutePath("@storybook/addon-links"),
|
||||||
|
getAbsolutePath("@storybook/addon-essentials"),
|
||||||
|
getAbsolutePath("@storybook/addon-interactions"),
|
||||||
|
getAbsolutePath("storybook-addon-rtl-direction"),
|
||||||
|
getAbsolutePath("storybook-react-i18next"),
|
||||||
|
],
|
||||||
|
|
||||||
|
framework: {
|
||||||
|
name: getAbsolutePath("@storybook/nextjs") as "@storybook/nextjs",
|
||||||
|
|
||||||
|
options: {
|
||||||
|
// builder: {
|
||||||
|
// fsCache: true,
|
||||||
|
// lazyCompilation: true,
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
staticDirs: ["../public"],
|
||||||
|
|
||||||
|
webpackFinal: async (config, { configType }) => {
|
||||||
|
config.resolve = config.resolve || {};
|
||||||
|
config.resolve.fallback = {
|
||||||
|
fs: false,
|
||||||
|
assert: false,
|
||||||
|
buffer: false,
|
||||||
|
console: false,
|
||||||
|
constants: false,
|
||||||
|
crypto: false,
|
||||||
|
domain: false,
|
||||||
|
events: false,
|
||||||
|
http: false,
|
||||||
|
https: false,
|
||||||
|
os: false,
|
||||||
|
path: false,
|
||||||
|
punycode: false,
|
||||||
|
process: false,
|
||||||
|
querystring: false,
|
||||||
|
stream: false,
|
||||||
|
string_decoder: false,
|
||||||
|
sys: false,
|
||||||
|
timers: false,
|
||||||
|
tty: false,
|
||||||
|
url: false,
|
||||||
|
util: false,
|
||||||
|
vm: false,
|
||||||
|
zlib: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
config.module = config.module || {};
|
||||||
|
config.module.rules = config.module.rules || [];
|
||||||
|
config.module.rules.push({
|
||||||
|
test: /\.css$/,
|
||||||
|
use: [
|
||||||
|
"style-loader",
|
||||||
|
{
|
||||||
|
loader: "css-loader",
|
||||||
|
options: {
|
||||||
|
modules: true, // Enable modules to help you using className
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
include: path.resolve(__dirname, "../src"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
|
||||||
|
typescript: { reactDocgen: "react-docgen" },
|
||||||
|
|
||||||
|
docs: {
|
||||||
|
autodocs: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|
||||||
|
function getAbsolutePath(value) {
|
||||||
|
return dirname(require.resolve(join(value, "package.json")));
|
||||||
|
}
|
||||||
Loaded 100 of 1240 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user