mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
779
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0939b92c3 | ||
|
|
a0274a7b6f | ||
|
|
bb4fdefee7 | ||
|
|
a85c5e582c | ||
|
|
f0e2a8b2c4 | ||
|
|
2f12717f30 | ||
|
|
838dbc52e3 | ||
|
|
e7d9ef74c4 | ||
|
|
d106e1d5bb | ||
|
|
00512df4ca | ||
|
|
038be90969 | ||
|
|
481eaf7530 | ||
|
|
2390d40731 | ||
|
|
4f74394e8f | ||
|
|
affd5876ab | ||
|
|
af531ee4f9 | ||
|
|
72bb6378c2 | ||
|
|
bfa1960d8a | ||
|
|
e530bf41f7 | ||
|
|
0d27c59cb8 | ||
|
|
97e611029e | ||
|
|
efc37c68ec | ||
|
|
5b6dd5f75a | ||
|
|
2c973387c5 | ||
|
|
8a841a6971 | ||
|
|
c973ca0a5d | ||
|
|
af05ad5784 | ||
|
|
39af9c544d | ||
|
|
10b6955e98 | ||
|
|
7e963e8439 | ||
|
|
d9d5a81337 | ||
|
|
193891a7c9 | ||
|
|
70cb5ca68a | ||
|
|
c8a1746119 | ||
|
|
06eb550e98 | ||
|
|
6c89a05a60 | ||
|
|
1b993ede0f | ||
|
|
648ffcdcfa | ||
|
|
0be94a7aff | ||
|
|
eba145ee2d | ||
|
|
99d230c70d | ||
|
|
4ac1b68bbc | ||
|
|
6adabae620 | ||
|
|
5f9dc0fcd8 | ||
|
|
da4dcd8505 | ||
|
|
d0f853d4cd | ||
|
|
2f272f0480 | ||
|
|
4317ee5617 | ||
|
|
1dc9217c65 | ||
|
|
50bd2877f4 | ||
|
|
f465e2267a | ||
|
|
b4f1652286 | ||
|
|
4e4860e7f8 | ||
|
|
cce04b3bd1 | ||
|
|
7568964b7c | ||
|
|
82af29c0cf | ||
|
|
c6672e93cf | ||
|
|
ac905a80b5 | ||
|
|
5df65d5417 | ||
|
|
abd6f0add2 | ||
|
|
25854c84f5 | ||
|
|
5724d19426 | ||
|
|
7c84337c5f | ||
|
|
b1d37bc4be | ||
|
|
4808cad081 | ||
|
|
72cd7671c4 | ||
|
|
c3e411fcaa | ||
|
|
8c31e9f206 | ||
|
|
a4ad1b6df3 | ||
|
|
9e94f15f02 | ||
|
|
e27719108b | ||
|
|
8bf2f54a51 | ||
|
|
8c03db70e9 | ||
|
|
8ea69e0321 | ||
|
|
dfec2b3cb7 | ||
|
|
69d778ab05 | ||
|
|
848f09956e | ||
|
|
51f50b161c | ||
|
|
53d3713bfe | ||
|
|
b8385fe927 | ||
|
|
52e3319a86 | ||
|
|
8b41aed919 | ||
|
|
b2233193d5 | ||
|
|
ba4430801d | ||
|
|
1c1ec43150 | ||
|
|
2f62cced0a | ||
|
|
afb8f70316 | ||
|
|
78eeac6d14 | ||
|
|
9f65177649 | ||
|
|
e9ea39a822 | ||
|
|
2097b0a0b0 | ||
|
|
78ef52cc9e | ||
|
|
f040a4dc31 | ||
|
|
56385ce167 | ||
|
|
bc592903db | ||
|
|
06a2d48deb | ||
|
|
c4d9c3dc4f | ||
|
|
b36f1edeb0 | ||
|
|
eef443f07e | ||
|
|
62f7f25cb5 | ||
|
|
4b94d14f1e | ||
|
|
12f76d74b1 | ||
|
|
9f504cd065 | ||
|
|
9dc27e9ca5 | ||
|
|
2bff4192ee | ||
|
|
a9fb17b067 | ||
|
|
db3d00b1da | ||
|
|
5ab42b8d32 | ||
|
|
8de7b41929 | ||
|
|
a5c0a920ba | ||
|
|
37a2b1cbd9 | ||
|
|
6b66923f18 | ||
|
|
adb4e4ef36 | ||
|
|
56165ee3d9 | ||
|
|
d86258a6f6 | ||
|
|
d2230f8fe7 | ||
|
|
69d53cb2d4 | ||
|
|
447375dcbf | ||
|
|
afab349a77 | ||
|
|
2a95cfd5a3 | ||
|
|
8a1f8c71b2 | ||
|
|
2793bcac38 | ||
|
|
13f48797d4 | ||
|
|
0a71efc244 | ||
|
|
fcc7a56f4a | ||
|
|
0aab3e9a43 | ||
|
|
be5eeb1aba | ||
|
|
08b43dce50 | ||
|
|
ad3bdee6c1 | ||
|
|
e00efaea68 | ||
|
|
be214f7968 | ||
|
|
64d182ac6a | ||
|
|
bb38bf4248 | ||
|
|
956285d522 | ||
|
|
dc704fdb51 | ||
|
|
e388d2f6a3 | ||
|
|
fc62ef2fcc | ||
|
|
b01ec03723 | ||
|
|
489b2c4e21 | ||
|
|
1a46fc2006 | ||
|
|
c1b7aff2d9 | ||
|
|
7df1814203 | ||
|
|
8f39783d7a | ||
|
|
4043153df3 | ||
|
|
9765c7f0e3 | ||
|
|
eddd159af8 | ||
|
|
17de886296 | ||
|
|
27b0e48296 | ||
|
|
6b2054d0b5 | ||
|
|
df0f7ba581 | ||
|
|
c484c487a9 | ||
|
|
47f3ab930b | ||
|
|
c8c3c68b0e | ||
|
|
facae65f08 | ||
|
|
ef6711e317 | ||
|
|
e06b06a21b | ||
|
|
c97dd78335 | ||
|
|
bf9bb22ac5 | ||
|
|
1b826df8f9 | ||
|
|
11f23fdcfa | ||
|
|
b5ec92964c | ||
|
|
71b74e21a3 | ||
|
|
1e797b3f01 | ||
|
|
f1b2dd7335 | ||
|
|
50c379c5c3 | ||
|
|
463e5acf50 | ||
|
|
807e8bec8b | ||
|
|
f167049b51 | ||
|
|
93b9500a95 | ||
|
|
935c1e40eb | ||
|
|
e472d98fe3 | ||
|
|
7fe65eec57 | ||
|
|
4cf375de6d | ||
|
|
19d0eb66c0 | ||
|
|
05d70f8826 | ||
|
|
98ec9e45c4 | ||
|
|
6dc17c91bc | ||
|
|
1493b7b8a6 | ||
|
|
965836c8b4 | ||
|
|
4545989c91 | ||
|
|
b71d42d226 | ||
|
|
945cdb3a99 | ||
|
|
a0db023860 | ||
|
|
6a7280e7dd | ||
|
|
680cb04de7 | ||
|
|
6e50ed050d | ||
|
|
f78b5c6596 | ||
|
|
55001a7daa | ||
|
|
6d2876ab26 | ||
|
|
b510194f00 | ||
|
|
8fd1c5d620 | ||
|
|
590f063694 | ||
|
|
90e1433878 | ||
|
|
2b42487198 | ||
|
|
53ed9462bd | ||
|
|
0da185e689 | ||
|
|
dc39af50a1 | ||
|
|
6da132db46 | ||
|
|
b2a02e3807 | ||
|
|
e7f78b3e01 | ||
|
|
682a56c03c | ||
|
|
7376c5b517 | ||
|
|
730c62bda6 | ||
|
|
db84c79802 | ||
|
|
6ba810a4da | ||
|
|
6d9b787816 | ||
|
|
7b8fb409b1 | ||
|
|
1fb78115e8 | ||
|
|
9567f1f4e8 | ||
|
|
49c93c635d | ||
|
|
5010cdedbb | ||
|
|
13413c05c3 | ||
|
|
dc185be836 | ||
|
|
29be9b6019 | ||
|
|
1247b1c719 | ||
|
|
23233d0dcb | ||
|
|
37540aa4f5 | ||
|
|
db0a971384 | ||
|
|
445e86361c | ||
|
|
80197b566c | ||
|
|
8671533878 | ||
|
|
f51f02375a | ||
|
|
d40df613de | ||
|
|
ebc3febc38 | ||
|
|
e77bfae091 | ||
|
|
f9efd803c1 | ||
|
|
54d6470163 | ||
|
|
50640b5bb9 | ||
|
|
4d4ba508ef | ||
|
|
4f2f9fedfa | ||
|
|
732538504d | ||
|
|
d3dcbfe567 | ||
|
|
905d2a8eaa | ||
|
|
b5e46c1a60 | ||
|
|
a3a8ad88fe | ||
|
|
9d6428d5d5 | ||
|
|
d7f8ca3f01 | ||
|
|
56bcc0d09f | ||
|
|
8b0c60d2f9 | ||
|
|
ffdfe4bbf8 | ||
|
|
556ca10ed9 | ||
|
|
193beb76fe | ||
|
|
8fcba8b5c9 | ||
|
|
6c8b34c230 | ||
|
|
b2884ddb88 | ||
|
|
82798fa5a3 | ||
|
|
377ade3c44 | ||
|
|
71533aaaf3 | ||
|
|
ccbe7e5d3f | ||
|
|
bea7739d7e | ||
|
|
76320d652a | ||
|
|
6d5968e2ea | ||
|
|
fd8fedca18 | ||
|
|
70c380fa36 | ||
|
|
512a7ae2bd | ||
|
|
49d3cd980f | ||
|
|
39a396810a | ||
|
|
c52ca65905 | ||
|
|
1d307178bb | ||
|
|
c3ed2ff6ce | ||
|
|
efadf17bfe | ||
|
|
bcfec8d5de | ||
|
|
4b2e22d9da | ||
|
|
45e22e2243 | ||
|
|
0a21f3b139 | ||
|
|
5d53685976 | ||
|
|
d869c39783 | ||
|
|
fe02964ebd | ||
|
|
cc8eb62c83 | ||
|
|
b1ad6ea87c | ||
|
|
d52d6c0bf0 | ||
|
|
86cfea0edb | ||
|
|
db406b0a27 | ||
|
|
2585518e33 | ||
|
|
a1342b4b9d | ||
|
|
c9fceb870c | ||
|
|
9f1ec356d1 | ||
|
|
28d285d5c5 | ||
|
|
a941d8b855 | ||
|
|
d40a355d9d | ||
|
|
3263133e5f | ||
|
|
f4f5494b85 | ||
|
|
fc0fd2f6ac | ||
|
|
70a75c2e7b | ||
|
|
6bc2168e29 | ||
|
|
a3ba66c90a | ||
|
|
c24d724000 | ||
|
|
df5ccc10ad | ||
|
|
2807d6cd9a | ||
|
|
20e2f2284f | ||
|
|
7a0d69c826 | ||
|
|
feedec80f2 | ||
|
|
18b1c344e6 | ||
|
|
f4d40a1289 | ||
|
|
1e65f075e6 | ||
|
|
68725d9262 | ||
|
|
5b805d686b | ||
|
|
584acafb1d | ||
|
|
dce7856b77 | ||
|
|
a6af609317 | ||
|
|
035f6244e1 | ||
|
|
723cb1a488 | ||
|
|
815209ebb0 | ||
|
|
53c74582fc | ||
|
|
78bbe77580 | ||
|
|
0c3871a681 | ||
|
|
b59eba4ec6 | ||
|
|
c6e32940f4 | ||
|
|
cb4c97f2ee | ||
|
|
2ee2ea09a0 | ||
|
|
a31cb67ec5 | ||
|
|
4ec84a147f | ||
|
|
e583de4650 | ||
|
|
98af2a5eb3 | ||
|
|
e3ad0f85de | ||
|
|
a26382ac51 | ||
|
|
9ba182d8a0 | ||
|
|
27fc8058b9 | ||
|
|
c5bd8c712f | ||
|
|
a1267b1bff | ||
|
|
18d17ea945 | ||
|
|
fabd9559c4 | ||
|
|
ec637d4417 | ||
|
|
5ecd15a5f5 | ||
|
|
4523e86b91 | ||
|
|
ec5472f1ed | ||
|
|
9ae804f8ae | ||
|
|
1ea108c2ce | ||
|
|
0a864e51b8 | ||
|
|
0cce480592 | ||
|
|
b622b092a8 | ||
|
|
96fe989f6d | ||
|
|
0de26be6e6 | ||
|
|
dc0a5c88b7 | ||
|
|
c6dd7de216 | ||
|
|
f77a64bff9 | ||
|
|
d13dbae0fe | ||
|
|
f552a1ba15 | ||
|
|
7282c5d51f | ||
|
|
82ec75d582 | ||
|
|
6bf6142667 | ||
|
|
938e65cb02 | ||
|
|
114de91f48 | ||
|
|
2e6f3f5c14 | ||
|
|
7b3d9bae03 | ||
|
|
d81a1154e3 | ||
|
|
a944c65660 | ||
|
|
856046dc82 | ||
|
|
2ca551123d | ||
|
|
c861d45749 | ||
|
|
4a43969da9 | ||
|
|
6fa4122910 | ||
|
|
1d27101770 | ||
|
|
9214decc8d | ||
|
|
ee4ae3bd47 | ||
|
|
53a114e7e4 | ||
|
|
f2df9cb93a | ||
|
|
d69726487e | ||
|
|
5fccb8cc28 | ||
|
|
f371237fd5 | ||
|
|
21a0d60c98 | ||
|
|
4eb8534294 | ||
|
|
c95f905bce | ||
|
|
0293da1e22 | ||
|
|
a4a0c4175d | ||
|
|
9548d18a3e | ||
|
|
d3c73fbb49 | ||
|
|
7928175387 | ||
|
|
8818187740 | ||
|
|
75324b24fc | ||
|
|
d6d118f4ab | ||
|
|
e998739998 | ||
|
|
dd29967e99 | ||
|
|
e7f5ae9306 | ||
|
|
4133ec99ae | ||
|
|
6dd63ee152 | ||
|
|
912dbda159 | ||
|
|
410a050244 | ||
|
|
9cf45816c2 | ||
|
|
386fc995e6 | ||
|
|
d6451b8fad | ||
|
|
920ddd18e6 | ||
|
|
ff9334ab78 | ||
|
|
c494e96f79 | ||
|
|
fd78f0f7df | ||
|
|
4ff10dcacc | ||
|
|
7a995fe759 | ||
|
|
227606baef | ||
|
|
f315e759f3 | ||
|
|
8a6fba75a5 | ||
|
|
196c76ac70 | ||
|
|
7f82484436 | ||
|
|
db18caf739 | ||
|
|
6693e28c93 | ||
|
|
bf489ce13b | ||
|
|
357d6482f9 | ||
|
|
eec72a915a | ||
|
|
ae29e4c424 | ||
|
|
05dc264df9 | ||
|
|
597b20a6cb | ||
|
|
6b7d9853f9 | ||
|
|
d6f51a96a5 | ||
|
|
20f80ac067 | ||
|
|
8bfa659120 | ||
|
|
65e44d76a1 | ||
|
|
bb07808661 | ||
|
|
d18550ff54 | ||
|
|
84dc921d41 | ||
|
|
e2d7dd837d | ||
|
|
9feb204fd5 | ||
|
|
3880f181b3 | ||
|
|
44a37e9e20 | ||
|
|
e6cfedd340 | ||
|
|
e4d1178e71 | ||
|
|
929bdcef25 | ||
|
|
3f725c6be9 | ||
|
|
990a32dd9e | ||
|
|
c5b8ed9cc1 | ||
|
|
fec573d23b | ||
|
|
a144968d70 | ||
|
|
6fa0724404 | ||
|
|
b1ce71e6b0 | ||
|
|
939092726c | ||
|
|
c6f8599c75 | ||
|
|
91d8519940 | ||
|
|
a90f79f1c1 | ||
|
|
f880b6e8c3 | ||
|
|
b8d65326e6 | ||
|
|
a9fd82e406 | ||
|
|
a8c2491fbb | ||
|
|
7f6486ec80 | ||
|
|
388cec1737 | ||
|
|
2f5a49a37d | ||
|
|
1f93184c42 | ||
|
|
7d9cf48ca3 | ||
|
|
eef2a7abdf | ||
|
|
c7a022ba43 | ||
|
|
96a84a8fd7 | ||
|
|
cea242c66f | ||
|
|
ce4eb563b3 | ||
|
|
2307d637fb | ||
|
|
0aa72aafae | ||
|
|
6e1f0d57af | ||
|
|
62b6ef06e6 | ||
|
|
f5ab593261 | ||
|
|
dd4a43b78c | ||
|
|
c4c56283e5 | ||
|
|
f71a98a527 | ||
|
|
2258668769 | ||
|
|
6dba407aed | ||
|
|
7eef845556 | ||
|
|
7781d377ae | ||
|
|
59bf4663ca | ||
|
|
6653dbbb92 | ||
|
|
d7d20ccb29 | ||
|
|
2d54986b09 | ||
|
|
9ea0b2bc2f | ||
|
|
715727853b | ||
|
|
43d4f709d5 | ||
|
|
f40050fe6f | ||
|
|
308a534d98 | ||
|
|
35a72c4f08 | ||
|
|
34dfcba45c | ||
|
|
cb1e31bcd4 | ||
|
|
3e4e7fc0cd | ||
|
|
da2444ede2 | ||
|
|
bc224de751 | ||
|
|
9403e4d65d | ||
|
|
07a49835a6 | ||
|
|
d32de6f217 | ||
|
|
9b00ce4f1d | ||
|
|
ca62504b5a | ||
|
|
8f6c8f9d8d | ||
|
|
23fcb284a1 | ||
|
|
ffdd0815a7 | ||
|
|
4260324153 | ||
|
|
d66813d3cf | ||
|
|
0ce92da41f | ||
|
|
e8ef4e40d7 | ||
|
|
d200b07466 | ||
|
|
92abfb31aa | ||
|
|
6c56eee0f4 | ||
|
|
399c37d943 | ||
|
|
f3acadd53c | ||
|
|
331fcee710 | ||
|
|
951a96ae01 | ||
|
|
ed1680837d | ||
|
|
7864a5b9d1 | ||
|
|
f11bd7ce0e | ||
|
|
fffc6795a7 | ||
|
|
8442a1e612 | ||
|
|
ed91549681 | ||
|
|
04173600ae | ||
|
|
d5fe5c34af | ||
|
|
6bc9adece8 | ||
|
|
97d46c6273 | ||
|
|
9d78a8e6b6 | ||
|
|
d12a6f5168 | ||
|
|
0f47e6ea28 | ||
|
|
77ceb15d7d | ||
|
|
592629587d | ||
|
|
187fde4a4e | ||
|
|
b74d4d45f9 | ||
|
|
aa9a58b44d | ||
|
|
59a773e9a0 | ||
|
|
e3053bb83d | ||
|
|
1f7fc4bd40 | ||
|
|
d1b59ff3f3 | ||
|
|
221b52bac0 | ||
|
|
b8ed30db55 | ||
|
|
be56317bf2 | ||
|
|
31728c9b72 | ||
|
|
9d0fc9e5b1 | ||
|
|
2dce141d17 | ||
|
|
d421b5ff30 | ||
|
|
b8eb0810c2 | ||
|
|
cb74a5756a | ||
|
|
d1549b225b | ||
|
|
ba25849871 | ||
|
|
44f3bbae03 | ||
|
|
2cd91f509b | ||
|
|
a06ba57384 | ||
|
|
9ac686ae31 | ||
|
|
7385bc1d9f | ||
|
|
dffcfe8ee4 | ||
|
|
e3f373b8d2 | ||
|
|
8b591f3522 | ||
|
|
452440f3c0 | ||
|
|
4e10cabec0 | ||
|
|
5d1da00138 | ||
|
|
b236e2546b | ||
|
|
412d09b801 | ||
|
|
5ab28ad99a | ||
|
|
9be447aacf | ||
|
|
66e68aea14 | ||
|
|
a332eb5f32 | ||
|
|
cc82b1a5cf | ||
|
|
ba32012744 | ||
|
|
775d21e5ee | ||
|
|
8f6530d1e7 | ||
|
|
4297c70f58 | ||
|
|
f6484294da | ||
|
|
16ed60a5c3 | ||
|
|
d831b93934 | ||
|
|
f3b9121a14 | ||
|
|
cf8519bfa5 | ||
|
|
860cf8cd57 | ||
|
|
c978b6f34f | ||
|
|
f3f2855fe5 | ||
|
|
3cb53f0604 | ||
|
|
a5f45b46e9 | ||
|
|
2c1813198d | ||
|
|
130c2221ec | ||
|
|
912ea37af6 | ||
|
|
ccc74dea74 | ||
|
|
c08e5f706c | ||
|
|
a6c6a5624a | ||
|
|
ee5bb6d73d | ||
|
|
bfbf180aee | ||
|
|
d8a3b53641 | ||
|
|
3018b478ba | ||
|
|
df4dbcf607 | ||
|
|
094fe90499 | ||
|
|
58a90cca4a | ||
|
|
29b0ebb3bd | ||
|
|
f0ccd1f586 | ||
|
|
90e705f8c5 | ||
|
|
17076e4a00 | ||
|
|
7c50bbc500 | ||
|
|
4df5f4a18f | ||
|
|
e8bcc94187 | ||
|
|
a071c715bc | ||
|
|
52981e0164 | ||
|
|
e576cd5082 | ||
|
|
387668bb47 | ||
|
|
30befc6a60 | ||
|
|
d13644a47a | ||
|
|
4a905a6ac9 | ||
|
|
b6e95f4502 | ||
|
|
1900492aad | ||
|
|
cb9d3869f0 | ||
|
|
96f4ef7d22 | ||
|
|
8e02a5a227 | ||
|
|
d17ae517d3 | ||
|
|
c2dd8aceda | ||
|
|
24fcf4247c | ||
|
|
2dc20ef0eb | ||
|
|
04943b6313 | ||
|
|
eb366cf94b | ||
|
|
77b4f8b7a0 | ||
|
|
24bd4c3d7b | ||
|
|
25d9891f2b | ||
|
|
a928fcd40f | ||
|
|
2d34301834 | ||
|
|
cf7259e060 | ||
|
|
8c9f9c6f58 | ||
|
|
33f8e35b62 | ||
|
|
2c1060f57a | ||
|
|
30e0ca81bd | ||
|
|
92cc382a6b | ||
|
|
c322cc3ffe | ||
|
|
7d428ffe83 | ||
|
|
fb96aecbef | ||
|
|
80d5d4716a | ||
|
|
c1dde09070 | ||
|
|
cf99269bc5 | ||
|
|
2a184288f4 | ||
|
|
9237f387fc | ||
|
|
d531c29fc8 | ||
|
|
b554243447 | ||
|
|
7044601603 | ||
|
|
53bc126015 | ||
|
|
f1dc468e50 | ||
|
|
15b2ab321e | ||
|
|
ce63b00cf4 | ||
|
|
b9aaecba25 | ||
|
|
789870b545 | ||
|
|
c9f7b7f0d9 | ||
|
|
cba7e53898 | ||
|
|
fb26da5df1 | ||
|
|
d9c606a1c2 | ||
|
|
eb8455bd76 | ||
|
|
a76a03d988 | ||
|
|
d9eb69aa47 | ||
|
|
a5b0378138 | ||
|
|
320068a576 | ||
|
|
3cd90ae629 | ||
|
|
e250b413f0 | ||
|
|
8ccfb1aebb | ||
|
|
9da4f19d4f | ||
|
|
44f33473bd | ||
|
|
358f1b78a8 | ||
|
|
cc04c682b0 | ||
|
|
66ba483b6a | ||
|
|
908e408480 | ||
|
|
1f6b9d94bf | ||
|
|
a734c8cd46 | ||
|
|
1788e1bd6c | ||
|
|
7f9ce1ca85 | ||
|
|
983adf3ffd | ||
|
|
f2ea0b9b48 | ||
|
|
e42a26fdad | ||
|
|
2c4770d29b | ||
|
|
2a9e423cb1 | ||
|
|
14869cc6d6 | ||
|
|
fe99470374 | ||
|
|
ff903e7cbb | ||
|
|
10ebf942b1 | ||
|
|
d9a75c5938 | ||
|
|
bb80e50d01 | ||
|
|
644840ec66 | ||
|
|
71e714c66b | ||
|
|
b2b187c57d | ||
|
|
ecc76c7520 | ||
|
|
898e8e214b | ||
|
|
43db2ddf6e | ||
|
|
7371b68418 | ||
|
|
7f4d5f692d | ||
|
|
8113854a89 | ||
|
|
eb097a037b | ||
|
|
cbfdd27834 | ||
|
|
113c266881 | ||
|
|
614db7388e | ||
|
|
cbc5259db3 | ||
|
|
0d6d6a0a35 | ||
|
|
e8d4065af5 | ||
|
|
008110b3f7 | ||
|
|
efb0cae4e0 | ||
|
|
ae99d59aba | ||
|
|
a06aaaf9ca | ||
|
|
4970d1b6f2 | ||
|
|
094cd180b5 | ||
|
|
a8f43f3486 | ||
|
|
038f4b5992 | ||
|
|
c903fb87ac | ||
|
|
e5841c4cc0 | ||
|
|
bb7815bd57 | ||
|
|
0c33d981a7 | ||
|
|
36d8ad6e7c | ||
|
|
fbce66d500 | ||
|
|
89db65299d | ||
|
|
ea6a05be4f | ||
|
|
355acf6824 | ||
|
|
c2988efa08 | ||
|
|
8ccf5094f8 | ||
|
|
39f2d24907 | ||
|
|
d3c5a425e7 | ||
|
|
85703d29e1 | ||
|
|
d3c11b5c93 | ||
|
|
6f177a0a7a | ||
|
|
dc48bb35ef | ||
|
|
8218f36d34 | ||
|
|
0fe906c538 | ||
|
|
4641596a6d | ||
|
|
d932527561 | ||
|
|
89c072e223 | ||
|
|
6421bc8689 | ||
|
|
d008d9bff0 | ||
|
|
f6dc6359bb | ||
|
|
31410879a7 | ||
|
|
343e352b17 | ||
|
|
b38ea72407 | ||
|
|
34f23bdc9f | ||
|
|
032638e896 | ||
|
|
10cc6a8040 | ||
|
|
9ecdbf5950 | ||
|
|
55b436c67b | ||
|
|
f74a60dba4 | ||
|
|
05defcc6e0 | ||
|
|
6700340ef3 | ||
|
|
4224f6ea5a | ||
|
|
8d925f3670 | ||
|
|
d2875c9bf6 | ||
|
|
c002f4afe3 | ||
|
|
ecb8104340 | ||
|
|
e6090800e2 | ||
|
|
92b7760e94 | ||
|
|
fe988f4489 | ||
|
|
0237cced6b | ||
|
|
625ec4ce91 | ||
|
|
5339592b3f | ||
|
|
9f1d092717 | ||
|
|
e38a4e4347 | ||
|
|
fdbc5b6737 | ||
|
|
fa2b7955d0 | ||
|
|
8b3d3c6f8b | ||
|
|
ea11af1e15 | ||
|
|
6cbae88131 | ||
|
|
03b2755aaf | ||
|
|
2095a94bc5 | ||
|
|
df7796d5cb | ||
|
|
d177e3adfc | ||
|
|
d04ac5c3e6 | ||
|
|
60939da801 | ||
|
|
cdb301a293 | ||
|
|
14a9aa890f | ||
|
|
d3833d06d5 | ||
|
|
5ba19ec763 | ||
|
|
0564936c4f | ||
|
|
c2795376d8 | ||
|
|
2b898dfe50 | ||
|
|
fa1404be52 | ||
|
|
be87675e1c | ||
|
|
b1edf75c4e | ||
|
|
8a0f092342 | ||
|
|
87b02de612 | ||
|
|
f8e9f71de3 | ||
|
|
c055c32371 | ||
|
|
d60858cd8f | ||
|
|
22bd655ad1 | ||
|
|
c900b22dc0 | ||
|
|
27e5ade92b | ||
|
|
d9c1b9db2a | ||
|
|
eb399c7bd0 | ||
|
|
b8e51366d7 | ||
|
|
b75cd993be | ||
|
|
a1bc503ce4 | ||
|
|
e475a7ac5a | ||
|
|
2dc24dabfd | ||
|
|
aaa2a0525f | ||
|
|
5e04480cc0 | ||
|
|
ea2b1519d5 | ||
|
|
e5385f7512 | ||
|
|
d8bf4b9f89 | ||
|
|
2d2c55e07c | ||
|
|
da6dff2d17 | ||
|
|
b4aab6134e | ||
|
|
1c465a8cda | ||
|
|
8ab6c2d83b | ||
|
|
c89d075200 | ||
|
|
6c6c71484d | ||
|
|
18a66bae4e | ||
|
|
68b72f3162 | ||
|
|
f6438065e3 | ||
|
|
4c6961f7d6 | ||
|
|
0bd27c4182 | ||
|
|
ddcfac4f0f | ||
|
|
3e58014dbd | ||
|
|
ff06920e24 |
@@ -0,0 +1,29 @@
|
||||
# Patch Release Checklist for Appwrite
|
||||
|
||||
When bumping a patch version (e.g., `1.9.0` -> `1.9.1`), follow this checklist.
|
||||
|
||||
## Checklist
|
||||
|
||||
### Bump console image
|
||||
|
||||
Update the console Docker image tag in both files:
|
||||
- [ ] `docker-compose.yml` -- update `image: appwrite/console:X.Y.Z`
|
||||
- [ ] `app/views/install/compose.phtml` -- update `image: <?php echo $organization; ?>/console:X.Y.Z`
|
||||
|
||||
### Bump Appwrite version
|
||||
|
||||
- [ ] **`app/init/constants.php`** -- update `APP_VERSION_STABLE` to the new version (e.g., `'1.9.1'`). In same file, increment `APP_CACHE_BUSTER` by 1.
|
||||
- [ ] **`README.md`** -- update the Docker image tag `appwrite/appwrite:X.Y.Z` in all 3 install code blocks (Unix, Windows CMD, PowerShell).
|
||||
- [ ] **`README-CN.md`** -- same Docker image tag update in all 3 install code blocks.
|
||||
- [ ] **`src/Appwrite/Migration/Migration.php`** -- add the new version to the `$versions` array, mapping it to a migration class. If new class exists, use that, otherwise use sle same class as previous version
|
||||
|
||||
### Update CHANGES.md
|
||||
|
||||
- [ ] Add a new `# Version X.Y.Z` section at the top of `CHANGES.md` with subsections: `### Notable changes`, `### Fixes`, `### Miscellaneous`
|
||||
|
||||
## Final review
|
||||
|
||||
- [ ] Ask user to review changes before commiting
|
||||
- [ ] Ask user to update `CHANGES.md` with PRs
|
||||
- [ ] Ask user to generate specs, if needed
|
||||
- [ ] Ask user to add request and response filters, if needed
|
||||
+66
-18
@@ -150,8 +150,37 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
|
||||
|
||||
- name: Cache PHPStan result cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .phpstan-cache
|
||||
key: phpstan-${{ github.sha }}
|
||||
restore-keys: |
|
||||
phpstan-
|
||||
|
||||
- name: Run PHPStan
|
||||
run: composer analyze
|
||||
run: composer analyze -- --no-progress
|
||||
|
||||
specs:
|
||||
name: Checks / Specs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
extensions: swoole
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
|
||||
|
||||
- name: Generate specs
|
||||
run: _APP_STORAGE_LIMIT=5368709120 php app/cli.php specs --version=latest --git=no
|
||||
|
||||
locale:
|
||||
name: Checks / Locale
|
||||
@@ -180,11 +209,38 @@ jobs:
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const allDatabases = ['MongoDB'];
|
||||
const allModes = ['shared_v1', 'shared_v2'];
|
||||
const allDatabases = ['MariaDB', 'PostgreSQL', 'MongoDB'];
|
||||
const allModes = ['dedicated', 'shared'];
|
||||
|
||||
core.setOutput('databases', JSON.stringify(allDatabases));
|
||||
core.setOutput('modes', JSON.stringify(allModes));
|
||||
const defaultDatabases = ['MongoDB'];
|
||||
const defaultModes = ['dedicated'];
|
||||
|
||||
const pr = context.payload.pull_request;
|
||||
if (!pr) {
|
||||
core.setOutput('databases', JSON.stringify(allDatabases));
|
||||
core.setOutput('modes', JSON.stringify(allModes));
|
||||
return;
|
||||
}
|
||||
|
||||
const getContent = (ref) => github.rest.repos.getContent({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
path: 'composer.lock',
|
||||
ref,
|
||||
});
|
||||
|
||||
const getDbVersion = (lock) => lock.packages?.find(p => p.name === 'utopia-php/database')?.version;
|
||||
|
||||
const [{ data: base }, { data: head }] = await Promise.all([
|
||||
getContent(pr.base.sha),
|
||||
getContent(pr.head.sha),
|
||||
]);
|
||||
|
||||
const decode = (content) => JSON.parse(Buffer.from(content, 'base64').toString());
|
||||
const databaseChanged = getDbVersion(decode(base.content)) !== getDbVersion(decode(head.content));
|
||||
|
||||
core.setOutput('databases', JSON.stringify(databaseChanged ? allDatabases : defaultDatabases));
|
||||
core.setOutput('modes', JSON.stringify(databaseChanged ? allModes : defaultModes));
|
||||
|
||||
build:
|
||||
name: Build
|
||||
@@ -423,11 +479,8 @@ jobs:
|
||||
env:
|
||||
_APP_BROWSER_HOST: http://invalid-browser/v1
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
@@ -456,7 +509,7 @@ jobs:
|
||||
# Services that rely on sequential test method execution (shared static state)
|
||||
FUNCTIONAL_FLAG="--functional"
|
||||
case "${{ matrix.service }}" in
|
||||
Databases|TablesDB|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
|
||||
Databases|TablesDB|Functions|Realtime|GraphQL|ProjectWebhooks) FUNCTIONAL_FLAG="" ;;
|
||||
esac
|
||||
|
||||
docker compose exec -T \
|
||||
@@ -501,11 +554,8 @@ jobs:
|
||||
env:
|
||||
_APP_OPTIONS_ABUSE: enabled
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
@@ -562,11 +612,8 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
@@ -645,10 +692,11 @@ jobs:
|
||||
|
||||
- name: Installing latest version
|
||||
run: |
|
||||
rm docker-compose.yml
|
||||
rm .env
|
||||
curl https://appwrite.io/install/compose -o docker-compose.yml
|
||||
curl https://appwrite.io/install/env -o .env
|
||||
LATEST_TAG=$(curl -fsSL -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/appwrite/appwrite/releases/latest | jq -r .tag_name)
|
||||
echo "Latest release tag: $LATEST_TAG"
|
||||
curl -fsSL "https://raw.githubusercontent.com/appwrite/appwrite/${LATEST_TAG}/docker-compose.yml" -o docker-compose.yml
|
||||
curl -fsSL "https://raw.githubusercontent.com/appwrite/appwrite/${LATEST_TAG}/.env" -o .env
|
||||
sed -i 's/_APP_OPTIONS_ABUSE=enabled/_APP_OPTIONS_ABUSE=disabled/g' .env
|
||||
docker compose up -d
|
||||
sleep 10
|
||||
|
||||
@@ -21,6 +21,7 @@ appwrite.config.json
|
||||
/app/config/specs/
|
||||
/docs/examples/
|
||||
.phpunit.cache
|
||||
.phpstan-cache
|
||||
playwright-report
|
||||
test-results
|
||||
docker-compose.web-installer.yml
|
||||
|
||||
@@ -115,6 +115,10 @@ Common injections: `$response`, `$request`, `$dbForProject`, `$dbForPlatform`, `
|
||||
- Never hardcode credentials -- use environment variables.
|
||||
- Code changes may require container restart. No central log location -- check relevant containers.
|
||||
|
||||
## Patch release process
|
||||
|
||||
For bumping patch versions (e.g., `1.9.0` -> `1.9.1`), follow the checklist in `.claude/skills/patch-release-checklist/SKILL.md`. It covers the 4 files that must be updated, console image bumps, CHANGES.md updates, and common pitfalls to avoid.
|
||||
|
||||
## Cross-repo context
|
||||
|
||||
Appwrite is the base server for `appwrite/cloud`. Changes to the Action pattern, module structure, DI system, or response models affect cloud. The `feat-dedicated-db` feature spans cloud, edge, and console.
|
||||
|
||||
+1
-1
@@ -892,7 +892,7 @@
|
||||
* Unset index length by @fogelito in https://github.com/appwrite/appwrite/pull/8978
|
||||
* Update base to 0.9.5 by @basert in https://github.com/appwrite/appwrite/pull/9005
|
||||
* Sync main into 1.6.x by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/9011
|
||||
* Improved shared tables V2 by @abnegate in https://github.com/appwrite/appwrite/pull/9013
|
||||
* Improved shared tables by @abnegate in https://github.com/appwrite/appwrite/pull/9013
|
||||
* Ensure backwards compatibility for 1.6.x by @christyjacob4 in https://github.com/appwrite/appwrite/pull/9018
|
||||
|
||||
# Version 1.6.0
|
||||
|
||||
@@ -1,43 +1,28 @@
|
||||
> We just announced DB operators for Appwrite Databases - [Learn more](https://appwrite.io/blog/post/announcing-db-operators)
|
||||
|
||||
> Appwrite Cloud is now Generally Available - [Learn more](https://appwrite.io/cloud-ga)
|
||||
|
||||
> [Get started with Appwrite](https://apwr.dev/appcloud)
|
||||
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/55a81268-4ecc-46cd-bdf5-73f7e8662fee" />
|
||||
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://appwrite.io" target="_blank"><img src="./public/images/banner.png" alt="Appwrite banner, with logo and text saying "The Developer's Cloud"></a>
|
||||
<br />
|
||||
<br />
|
||||
<b>Appwrite is a best-in-class, developer-first platform that gives builders everything they need to create scalable, stable, and production-ready software, fast.</b>
|
||||
<h1>Appwrite</h1>
|
||||
<b>Appwrite is an open-source, all-in-one development platform. Use built-in backend infrastructure and web hosting, all from a single place.</b>
|
||||
<br />
|
||||
<br />
|
||||
</p>
|
||||
|
||||
<!-- [](https://travis-ci.com/appwrite/appwrite) -->
|
||||
|
||||
[](https://appwrite.io/company/careers)
|
||||
[](https://hacktoberfest.appwrite.io)
|
||||
[](https://appwrite.io/discord?r=Github)
|
||||
[](https://github.com/appwrite/appwrite/actions)
|
||||
[](https://twitter.com/appwrite)
|
||||
|
||||
<!-- [](https://hub.docker.com/r/appwrite/appwrite) -->
|
||||
<!-- [](docs/tutorials/add-translations.md) -->
|
||||
<!-- [](https://store.appwrite.io) -->
|
||||
[](https://appwrite.io/discord)
|
||||
[](https://x.com/appwrite)
|
||||
[](https://cloud.appwrite.io)
|
||||
|
||||
English | [简体中文](README-CN.md)
|
||||
|
||||
Appwrite is an end-to-end platform for building Web, Mobile, Native, or Backend apps, packaged as a set of Docker microservices. It includes both a backend server and a fully integrated hosting solution for deploying static and server-side rendered frontends. Appwrite abstracts the complexity and repetitiveness required to build modern apps from scratch and allows you to build secure, full-stack applications faster.
|
||||
Appwrite is an open-source development platform for building web, mobile, and AI applications. It brings together backend infrastructure and web hosting in one place, so teams can build, ship, and scale without stitching together a fragmented stack. Appwrite is available as a managed cloud platform and can also be self-hosted on infrastructure you control.
|
||||
|
||||
Using Appwrite, you can easily integrate your app with user authentication and multiple sign-in methods, a database for storing and querying users and team data, storage and file management, image manipulation, Cloud Functions, messaging, and [more services](https://appwrite.io/docs).
|
||||
With Appwrite, you can add authentication, databases, storage, functions, messaging, realtime capabilities, and integrated web app hosting through Sites. It is designed to reduce the repetitive backend work required to launch modern products while giving developers secure primitives and flexible APIs to build production-ready applications faster.
|
||||
|
||||

|
||||
|
||||
Find out more at: [https://appwrite.io](https://appwrite.io).
|
||||
Find out more at [https://appwrite.io](https://appwrite.io).
|
||||
|
||||
Table of Contents:
|
||||
|
||||
- [Products](#products)
|
||||
- [Installation \& Setup](#installation--setup)
|
||||
- [Self-Hosting](#self-hosting)
|
||||
- [Unix](#unix)
|
||||
@@ -47,17 +32,31 @@ Table of Contents:
|
||||
- [Upgrade from an Older Version](#upgrade-from-an-older-version)
|
||||
- [One-Click Setups](#one-click-setups)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Products](#products)
|
||||
- [SDKs](#sdks)
|
||||
- [Client](#client)
|
||||
- [Server](#server)
|
||||
- [Community](#community)
|
||||
- [Architecture](#architecture)
|
||||
- [Contributing](#contributing)
|
||||
- [Security](#security)
|
||||
- [Follow Us](#follow-us)
|
||||
- [License](#license)
|
||||
|
||||
|
||||
## Products
|
||||
|
||||
- **[Appwrite Auth](https://appwrite.io/docs/products/auth)** - Secure user authentication with multiple login methods including email/password, SMS, OAuth, anonymous sessions, and magic links. Includes session management, multi-factor authentication, and user verification flows.
|
||||
|
||||
- **[Appwrite Databases](https://appwrite.io/docs/products/databases)** - Scalable structured data storage with support for databases, tables, and rows. Includes querying, pagination, indexing, and relationships to model complex application data.
|
||||
|
||||
- **[Appwrite Storage](https://appwrite.io/docs/products/storage)** - Secure file storage with support for uploads, downloads, encryption, compression, and file transformations for media and assets.
|
||||
|
||||
- **[Appwrite Functions](https://appwrite.io/docs/products/functions)** - Serverless compute platform to run custom backend logic in isolated runtimes, triggered by events or scheduled jobs.15 runtimes supported.
|
||||
|
||||
- **[Appwrite Messaging](https://appwrite.io/docs/products/messaging)** - Multi-channel messaging system for sending emails, SMS, and push notifications to users for engagement, alerts, and transactional workflows.
|
||||
|
||||
- **[Appwrite Sites](https://appwrite.io/docs/products/sites)** - Integrated hosting platform to deploy and scale web applications with support for custom domains, SSR, and seamless backend integration. Git integration and previews are supported.
|
||||
|
||||
|
||||
## Installation & Setup
|
||||
|
||||
The easiest way to get started with Appwrite is by [signing up for Appwrite Cloud](https://cloud.appwrite.io/). While Appwrite Cloud is in public beta, you can build with Appwrite completely free, and we won't collect your credit card information.
|
||||
@@ -72,6 +71,7 @@ Before running the installation command, make sure you have [Docker](https://www
|
||||
|
||||
```bash
|
||||
docker run -it --rm \
|
||||
--publish 20080:20080 \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
|
||||
--entrypoint="install" \
|
||||
@@ -84,6 +84,7 @@ docker run -it --rm \
|
||||
|
||||
```cmd
|
||||
docker run -it --rm ^
|
||||
--publish 20080:20080 ^
|
||||
--volume //var/run/docker.sock:/var/run/docker.sock ^
|
||||
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
|
||||
--entrypoint="install" ^
|
||||
@@ -94,6 +95,7 @@ docker run -it --rm ^
|
||||
|
||||
```powershell
|
||||
docker run -it --rm `
|
||||
--publish 20080:20080 `
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock `
|
||||
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
|
||||
--entrypoint="install" `
|
||||
@@ -165,51 +167,29 @@ Getting started with Appwrite is as easy as creating a new project, choosing you
|
||||
| | [Quick start for Kotlin](https://appwrite.io/docs/quick-starts/kotlin) |
|
||||
| | [Quick start for Swift](https://appwrite.io/docs/quick-starts/swift) |
|
||||
|
||||
### Products
|
||||
|
||||
- [**Account**](https://appwrite.io/docs/references/cloud/client-web/account) - Manage current user authentication and account. Track and manage the user sessions, devices, sign-in methods, and security logs.
|
||||
- [**Users**](https://appwrite.io/docs/server/users) - Manage and list all project users when building backend integrations with Server SDKs.
|
||||
- [**Teams**](https://appwrite.io/docs/references/cloud/client-web/teams) - Manage and group users in teams. Manage memberships, invites, and user roles within a team.
|
||||
- [**Databases**](https://appwrite.io/docs/references/cloud/client-web/databases) - Manage databases, collections, and documents. Read, create, update, and delete documents and filter lists of document collections using advanced filters.
|
||||
- [**Storage**](https://appwrite.io/docs/references/cloud/client-web/storage) - Manage storage files. Read, create, delete, and preview files. Manipulate the preview of your files to perfectly fit your app. All files are scanned by ClamAV and stored in a secure and encrypted way.
|
||||
- [**Functions**](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Customize your Appwrite project by executing your custom code in a secure, isolated environment. You can trigger your code on any Appwrite system event either manually or using a CRON schedule.
|
||||
- [**Messaging**](https://appwrite.io/docs/references/cloud/client-web/messaging) - Communicate with your users through push notifications, emails, and SMS text messages using Appwrite Messaging.
|
||||
- [**Realtime**](https://appwrite.io/docs/realtime) - Listen to real-time events for any of your Appwrite services including users, storage, functions, databases, and more.
|
||||
- [**Locale**](https://appwrite.io/docs/references/cloud/client-web/locale) - Track your user's location and manage your app locale-based data.
|
||||
- [**Avatars**](https://appwrite.io/docs/references/cloud/client-web/avatars) - Manage your users' avatars, countries' flags, browser icons, and credit card symbols. Generate QR codes from links or plaintext strings.
|
||||
- [**MCP**](https://appwrite.io/docs/tooling/mcp) - Use Appwrite's Model Context Protocol (MCP) server to allow LLMs and AI tools like Claude Desktop, Cursor, and Windsurf Editor to directly interact with your Appwrite project through natural language.
|
||||
- [**Sites**](https://appwrite.io/docs/products/sites) - Develop, deploy, and scale your web applications directly from Appwrite, alongside your backend.
|
||||
|
||||
For the complete API documentation, visit [https://appwrite.io/docs](https://appwrite.io/docs). For more tutorials, news and announcements check out our [blog](https://medium.com/appwrite-io) and [Discord Server](https://discord.gg/GSeTUeA).
|
||||
|
||||
### SDKs
|
||||
|
||||
Below is a list of currently supported platforms and languages. If you would like to help us add support to your platform of choice, you can go over to our [SDK Generator](https://github.com/appwrite/sdk-generator) project and view our [contribution guide](https://github.com/appwrite/sdk-generator/blob/master/CONTRIBUTING.md).
|
||||
|
||||
#### Client
|
||||
|
||||
- :white_check_mark: [Web](https://github.com/appwrite/sdk-for-web) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Flutter](https://github.com/appwrite/sdk-for-flutter) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Apple](https://github.com/appwrite/sdk-for-apple) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Android](https://github.com/appwrite/sdk-for-android) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [React Native](https://github.com/appwrite/sdk-for-react-native) - **Beta** (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Web](https://github.com/appwrite/sdk-for-web)
|
||||
- :white_check_mark: [Flutter](https://github.com/appwrite/sdk-for-flutter)
|
||||
- :white_check_mark: [Apple](https://github.com/appwrite/sdk-for-apple)
|
||||
- :white_check_mark: [Android](https://github.com/appwrite/sdk-for-android)
|
||||
- :white_check_mark: [React Native](https://github.com/appwrite/sdk-for-react-native)
|
||||
|
||||
#### Server
|
||||
|
||||
- :white_check_mark: [NodeJS](https://github.com/appwrite/sdk-for-node) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [PHP](https://github.com/appwrite/sdk-for-php) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Dart](https://github.com/appwrite/sdk-for-dart) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Deno](https://github.com/appwrite/sdk-for-deno) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Ruby](https://github.com/appwrite/sdk-for-ruby) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Python](https://github.com/appwrite/sdk-for-python) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Kotlin](https://github.com/appwrite/sdk-for-kotlin) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Swift](https://github.com/appwrite/sdk-for-swift) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [.NET](https://github.com/appwrite/sdk-for-dotnet) - **Beta** (Maintained by the Appwrite Team)
|
||||
|
||||
#### Community
|
||||
|
||||
- :white_check_mark: [Appcelerator Titanium](https://github.com/m1ga/ti.appwrite) (Maintained by [Michael Gangolf](https://github.com/m1ga/))
|
||||
- :white_check_mark: [Godot Engine](https://github.com/GodotNuts/appwrite-sdk) (Maintained by [fenix-hub @GodotNuts](https://github.com/fenix-hub))
|
||||
- :white_check_mark: [NodeJS](https://github.com/appwrite/sdk-for-node)
|
||||
- :white_check_mark: [PHP](https://github.com/appwrite/sdk-for-php)
|
||||
- :white_check_mark: [Dart](https://github.com/appwrite/sdk-for-dart)
|
||||
- :white_check_mark: [Deno](https://github.com/appwrite/sdk-for-deno)
|
||||
- :white_check_mark: [Ruby](https://github.com/appwrite/sdk-for-ruby)
|
||||
- :white_check_mark: [Python](https://github.com/appwrite/sdk-for-python)
|
||||
- :white_check_mark: [Kotlin](https://github.com/appwrite/sdk-for-kotlin)
|
||||
- :white_check_mark: [Swift](https://github.com/appwrite/sdk-for-swift)
|
||||
- :white_check_mark: [.NET](https://github.com/appwrite/sdk-for-dotnet)
|
||||
|
||||
Looking for more SDKs? - Help us by contributing a pull request to our [SDK Generator](https://github.com/appwrite/sdk-generator)!
|
||||
|
||||
|
||||
+47
-45
@@ -2,12 +2,12 @@
|
||||
|
||||
require_once __DIR__ . '/init.php';
|
||||
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
|
||||
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\StatsResources;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
use Appwrite\Runtimes\Runtimes;
|
||||
use Appwrite\Usage\Context as UsageContext;
|
||||
@@ -18,13 +18,15 @@ use Swoole\Timer;
|
||||
use Utopia\Cache\Adapter\Pool as CachePool;
|
||||
use Utopia\Cache\Adapter\Sharding;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\CLI\Adapters\Generic;
|
||||
use Utopia\CLI\CLI;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Adapter\Pool as DatabasePool;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DI\Dependency;
|
||||
use Utopia\DI\Container;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Platform\Service;
|
||||
@@ -47,7 +49,7 @@ require_once __DIR__ . '/controllers/general.php';
|
||||
global $register;
|
||||
|
||||
$platform = new Appwrite();
|
||||
$args = $platform->getEnv('argv');
|
||||
$args = $_SERVER['argv'] ?? [];
|
||||
|
||||
\array_shift($args);
|
||||
if (! isset($args[0])) {
|
||||
@@ -56,21 +58,15 @@ if (! isset($args[0])) {
|
||||
}
|
||||
|
||||
$taskName = $args[0];
|
||||
$container = new Container();
|
||||
$cli = new CLI(new Generic(), $_SERVER['argv'] ?? [], $container);
|
||||
|
||||
$platform->setCli($cli);
|
||||
$platform->init(Service::TYPE_TASK);
|
||||
$cli = $platform->getCli();
|
||||
|
||||
$setResource = function (string $name, callable $callback, array $injections = []) use ($cli) {
|
||||
$dependency = new Dependency();
|
||||
$dependency->setName($name)->setCallback($callback);
|
||||
foreach ($injections as $injection) {
|
||||
$dependency->inject($injection);
|
||||
}
|
||||
$cli->setResource($dependency);
|
||||
};
|
||||
$container->set('register', fn () => $register, []);
|
||||
|
||||
$setResource('register', fn () => $register, []);
|
||||
|
||||
$setResource('cache', function ($pools) {
|
||||
$container->set('cache', function ($pools) {
|
||||
$list = Config::getParam('pools-cache', []);
|
||||
$adapters = [];
|
||||
|
||||
@@ -81,18 +77,18 @@ $setResource('cache', function ($pools) {
|
||||
return new Cache(new Sharding($adapters));
|
||||
}, ['pools']);
|
||||
|
||||
$setResource('pools', function (Registry $register) {
|
||||
$container->set('pools', function (Registry $register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
|
||||
$setResource('authorization', function () {
|
||||
$container->set('authorization', function () {
|
||||
$authorization = new Authorization();
|
||||
$authorization->disable();
|
||||
|
||||
return $authorization;
|
||||
}, []);
|
||||
|
||||
$setResource('dbForPlatform', function ($pools, $cache, $authorization) {
|
||||
$container->set('dbForPlatform', function ($pools, $cache, $authorization) {
|
||||
$sleep = 3;
|
||||
$maxAttempts = 5;
|
||||
$attempts = 0;
|
||||
@@ -135,17 +131,17 @@ $setResource('dbForPlatform', function ($pools, $cache, $authorization) {
|
||||
return $dbForPlatform;
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
$setResource('console', function () {
|
||||
$container->set('console', function () {
|
||||
return new Document(Config::getParam('console'));
|
||||
}, []);
|
||||
|
||||
$setResource(
|
||||
$container->set(
|
||||
'isResourceBlocked',
|
||||
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false,
|
||||
[]
|
||||
);
|
||||
|
||||
$setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) {
|
||||
$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) {
|
||||
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
|
||||
|
||||
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) {
|
||||
@@ -207,10 +203,10 @@ $setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $c
|
||||
};
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
|
||||
|
||||
$setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
$database = null;
|
||||
|
||||
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
|
||||
return function (?Document $project = null) use ($pools, $cache, &$database, $authorization) {
|
||||
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant($project->getSequence());
|
||||
return $database;
|
||||
@@ -235,41 +231,43 @@ $setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $a
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
$setResource('publisher', function (Group $pools) {
|
||||
$container->set('publisher', function (Group $pools) {
|
||||
return new BrokerPool(publisher: $pools->get('publisher'));
|
||||
}, ['pools']);
|
||||
$setResource('publisherDatabases', function (BrokerPool $publisher) {
|
||||
$container->set('publisherDatabases', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
$setResource('publisherFunctions', function (BrokerPool $publisher) {
|
||||
$container->set('publisherFunctions', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
$setResource('publisherMigrations', function (BrokerPool $publisher) {
|
||||
$container->set('publisherMigrations', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
$setResource('publisherMessaging', function (BrokerPool $publisher) {
|
||||
$container->set('publisherMessaging', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
$setResource('usage', function () {
|
||||
$container->set('usage', function () {
|
||||
return new UsageContext();
|
||||
}, []);
|
||||
$setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
|
||||
$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$setResource('queueForStatsResources', function (Publisher $publisher) {
|
||||
return new StatsResources($publisher);
|
||||
}, ['publisher']);
|
||||
$setResource('queueForFunctions', function (Publisher $publisher) {
|
||||
$container->set('publisherForCertificates', fn (Publisher $publisher) => new CertificatePublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$container->set('publisherForStatsResources', fn (Publisher $publisher) => new StatsResourcesPublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$container->set('queueForFunctions', function (Publisher $publisher) {
|
||||
return new Func($publisher);
|
||||
}, ['publisher']);
|
||||
$setResource('queueForDeletes', function (Publisher $publisher) {
|
||||
$container->set('queueForDeletes', function (Publisher $publisher) {
|
||||
return new Delete($publisher);
|
||||
}, ['publisher']);
|
||||
$setResource('queueForCertificates', function (Publisher $publisher) {
|
||||
return new Certificate($publisher);
|
||||
}, ['publisher']);
|
||||
$setResource('logError', function (Registry $register) {
|
||||
$container->set('logError', function (Registry $register) {
|
||||
return function (Throwable $error, string $namespace, string $action) use ($register) {
|
||||
Console::error('[Error] Timestamp: ' . date('c', time()));
|
||||
Console::error('[Error] Type: ' . get_class($error));
|
||||
@@ -321,25 +319,28 @@ $setResource('logError', function (Registry $register) {
|
||||
};
|
||||
}, ['register']);
|
||||
|
||||
$setResource('executor', fn () => new Executor(), []);
|
||||
$container->set('executor', fn () => new Executor(), []);
|
||||
|
||||
$setResource('bus', function (Registry $register) use ($cli) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $cli->getResource($name));
|
||||
$container->set('bus', function (Registry $register) use ($container) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $container->get($name));
|
||||
}, ['register']);
|
||||
|
||||
$setResource('telemetry', fn () => new NoTelemetry(), []);
|
||||
$container->set('telemetry', fn () => new NoTelemetry(), []);
|
||||
|
||||
$exitCode = 0;
|
||||
|
||||
$cli
|
||||
->error()
|
||||
->inject('error')
|
||||
->inject('logError')
|
||||
->action(function (Throwable $error, callable $logError) use ($taskName) {
|
||||
->action(function (Throwable $error, callable $logError) use ($taskName, &$exitCode) {
|
||||
call_user_func_array($logError, [
|
||||
$error,
|
||||
'Task',
|
||||
$taskName,
|
||||
]);
|
||||
|
||||
$exitCode = 1;
|
||||
Timer::clearAll();
|
||||
});
|
||||
|
||||
@@ -348,3 +349,4 @@ $cli->shutdown()->action(fn () => Timer::clearAll());
|
||||
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
|
||||
require_once __DIR__ . '/init/span.php';
|
||||
run($cli->run(...));
|
||||
Console::exit($exitCode);
|
||||
|
||||
@@ -594,7 +594,7 @@ $platformCollections = [
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('key'),
|
||||
'$id' => ID::custom('key'), // For app platforms
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
@@ -605,7 +605,7 @@ $platformCollections = [
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('store'),
|
||||
'$id' => ID::custom('store'), // Unused at the moment
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 256,
|
||||
@@ -616,7 +616,7 @@ $platformCollections = [
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('hostname'),
|
||||
'$id' => ID::custom('hostname'), // For web platforms
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 256,
|
||||
|
||||
@@ -39,6 +39,10 @@ $console = [
|
||||
'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user
|
||||
'duration' => TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
|
||||
'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled',
|
||||
// For email configuration, false means feature is disabled; false means these emails are allowed during sign-ups
|
||||
'disposableEmails' => false,
|
||||
'canonicalEmails' => false,
|
||||
'freeEmails' => false,
|
||||
'invalidateSessions' => true
|
||||
],
|
||||
'authWhitelistEmails' => (!empty(System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [],
|
||||
|
||||
@@ -226,6 +226,21 @@ return [
|
||||
'description' => 'A user with the same email already exists in the current project.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::USER_EMAIL_DISPOSABLE => [
|
||||
'name' => Exception::USER_EMAIL_DISPOSABLE,
|
||||
'description' => 'Disposable email addresses are not allowed. Please use a permanent email address.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::USER_EMAIL_FREE => [
|
||||
'name' => Exception::USER_EMAIL_FREE,
|
||||
'description' => 'Free email addresses are not allowed. Please use a business or custom-domain email address.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::USER_EMAIL_NOT_CANONICAL => [
|
||||
'name' => Exception::USER_EMAIL_NOT_CANONICAL,
|
||||
'description' => 'This email address must already be in its canonical form. Please remove aliases, tags, or provider-specific variations and try again.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::USER_PASSWORD_MISMATCH => [
|
||||
'name' => Exception::USER_PASSWORD_MISMATCH,
|
||||
'description' => 'Passwords do not match. Please check the password and confirm password.',
|
||||
@@ -1164,6 +1179,16 @@ return [
|
||||
'description' => 'Platform with the requested ID could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::PLATFORM_METHOD_UNSUPPORTED => [
|
||||
'name' => Exception::PLATFORM_METHOD_UNSUPPORTED,
|
||||
'description' => 'The requested platform has invalid type. Please use corresponding update method for the platform type.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::PLATFORM_ALREADY_EXISTS => [
|
||||
'name' => Exception::PLATFORM_ALREADY_EXISTS,
|
||||
'description' => 'Platform with the same ID already exists in this project. Try again with a different ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::VARIABLE_NOT_FOUND => [
|
||||
'name' => Exception::VARIABLE_NOT_FOUND,
|
||||
'description' => 'Variable with the requested ID could not be found.',
|
||||
|
||||
@@ -9,11 +9,5 @@ return [
|
||||
'mfaChallenge',
|
||||
'sessionAlert',
|
||||
'otpSession'
|
||||
],
|
||||
'sms' => [
|
||||
'verification',
|
||||
'login',
|
||||
'invitation',
|
||||
'mfaChallenge'
|
||||
]
|
||||
];
|
||||
|
||||
@@ -376,6 +376,17 @@ return [
|
||||
'mock' => false,
|
||||
'class' => 'Appwrite\\Auth\\OAuth2\\Wordpress',
|
||||
],
|
||||
'x' => [
|
||||
'name' => 'X',
|
||||
'developers' => 'https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code',
|
||||
'icon' => 'icon-twitter',
|
||||
'enabled' => true,
|
||||
'sandbox' => false,
|
||||
'form' => false,
|
||||
'beta' => false,
|
||||
'mock' => false,
|
||||
'class' => 'Appwrite\\Auth\\OAuth2\\X',
|
||||
],
|
||||
'yahoo' => [
|
||||
'name' => 'Yahoo',
|
||||
'developers' => 'https://developer.yahoo.com/oauth2/guide/flows_authcode/',
|
||||
|
||||
@@ -9,8 +9,8 @@ return [
|
||||
'key' => 'graphql',
|
||||
'name' => 'GraphQL',
|
||||
],
|
||||
'realtime' => [
|
||||
'key' => 'realtime',
|
||||
'name' => 'Realtime',
|
||||
'websocket' => [
|
||||
'key' => 'websocket',
|
||||
'name' => 'Websocket',
|
||||
],
|
||||
];
|
||||
@@ -55,6 +55,9 @@ $admins = [
|
||||
'tables.write',
|
||||
'platforms.read',
|
||||
'platforms.write',
|
||||
'policies.write',
|
||||
'templates.read',
|
||||
'templates.write',
|
||||
'projects.write',
|
||||
'keys.read',
|
||||
'keys.write',
|
||||
|
||||
@@ -3,13 +3,6 @@
|
||||
// List of scopes for organization (teams) API keys
|
||||
|
||||
return [
|
||||
"platforms.read" => [
|
||||
"description" => 'Access to read project\'s platforms',
|
||||
],
|
||||
"platforms.write" => [
|
||||
"description" =>
|
||||
'Access to create, update, and delete project\'s platforms',
|
||||
],
|
||||
"projects.read" => [
|
||||
"description" => 'Access to read organization\'s projects',
|
||||
],
|
||||
@@ -17,13 +10,6 @@ return [
|
||||
"description" =>
|
||||
"Access to create, update, and delete projects in organization",
|
||||
],
|
||||
"keys.read" => [
|
||||
"description" => 'Access to read project\'s API keys',
|
||||
],
|
||||
"keys.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s API keys",
|
||||
],
|
||||
"devKeys.read" => [
|
||||
"description" => 'Access to read project\'s development keys',
|
||||
],
|
||||
|
||||
@@ -188,4 +188,32 @@ return [ // List of publicly visible scopes
|
||||
"description" =>
|
||||
"Access to update project\'s information",
|
||||
],
|
||||
"keys.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s keys",
|
||||
],
|
||||
"keys.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s keys",
|
||||
],
|
||||
"platforms.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s platforms",
|
||||
],
|
||||
"platforms.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s platforms",
|
||||
],
|
||||
"policies.write" => [
|
||||
"description" =>
|
||||
"Access to update project\'s policies",
|
||||
],
|
||||
"templates.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s templates",
|
||||
],
|
||||
"templates.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s templates",
|
||||
],
|
||||
];
|
||||
|
||||
@@ -300,6 +300,26 @@ return [
|
||||
'repoBranch' => 'main',
|
||||
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/cursor-plugin/CHANGELOG.md'),
|
||||
],
|
||||
[
|
||||
'key' => 'claude-plugin',
|
||||
'name' => 'ClaudePlugin',
|
||||
'version' => '0.1.0',
|
||||
'url' => 'https://github.com/appwrite/claude-plugin.git',
|
||||
'enabled' => true,
|
||||
'beta' => false,
|
||||
'dev' => false,
|
||||
'hidden' => false,
|
||||
'spec' => 'static',
|
||||
'family' => APP_SDK_PLATFORM_STATIC,
|
||||
'prism' => 'claude-plugin',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/static-claude-plugin'),
|
||||
'gitUrl' => 'git@github.com:appwrite/claude-plugin.git',
|
||||
'gitRepoName' => 'claude-plugin',
|
||||
'gitUserName' => 'appwrite',
|
||||
'gitBranch' => 'dev',
|
||||
'repoBranch' => 'main',
|
||||
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/claude-plugin/CHANGELOG.md'),
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ return [
|
||||
'docs' => true,
|
||||
'docsUrl' => '',
|
||||
'tests' => false,
|
||||
'optional' => false,
|
||||
'optional' => true,
|
||||
'icon' => '',
|
||||
'platforms' => ['client', 'server', 'console'],
|
||||
],
|
||||
@@ -193,7 +193,7 @@ return [
|
||||
'docs' => false,
|
||||
'docsUrl' => '',
|
||||
'tests' => false,
|
||||
'optional' => false,
|
||||
'optional' => true,
|
||||
'icon' => '',
|
||||
'platforms' => ['client', 'server', 'console'],
|
||||
],
|
||||
@@ -235,7 +235,7 @@ return [
|
||||
'docs' => true,
|
||||
'docsUrl' => 'https://appwrite.io/docs/proxy',
|
||||
'tests' => false,
|
||||
'optional' => false,
|
||||
'optional' => true,
|
||||
'icon' => '/images/services/proxy.png',
|
||||
'platforms' => ['client', 'server', 'console'],
|
||||
],
|
||||
@@ -291,7 +291,7 @@ return [
|
||||
'docs' => true,
|
||||
'docsUrl' => 'https://appwrite.io/docs/migrations',
|
||||
'tests' => true,
|
||||
'optional' => false,
|
||||
'optional' => true,
|
||||
'icon' => '/images/services/migrations.png',
|
||||
'platforms' => ['client', 'server', 'console'],
|
||||
],
|
||||
|
||||
@@ -31,9 +31,6 @@ class FunctionUseCases
|
||||
public const DEV_TOOLS = 'dev-tools';
|
||||
public const AUTH = 'auth';
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
public static function getAll(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -25,9 +25,6 @@ class SiteUseCases
|
||||
public const FORMS = 'forms';
|
||||
public const DASHBOARD = 'dashboard';
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
public static function getAll(): array
|
||||
{
|
||||
return [
|
||||
@@ -252,7 +249,7 @@ return [
|
||||
'frameworks' => [
|
||||
getFramework('VITE', [
|
||||
'providerRootDirectory' => './vite/vitepress',
|
||||
'outputDirectory' => '404.html',
|
||||
'fallbackFile' => '404.html',
|
||||
'installCommand' => 'npm i vitepress && npm install',
|
||||
'buildCommand' => 'npm run docs:build',
|
||||
'outputDirectory' => './.vitepress/dist',
|
||||
@@ -275,7 +272,7 @@ return [
|
||||
'frameworks' => [
|
||||
getFramework('VUE', [
|
||||
'providerRootDirectory' => './vue/vuepress',
|
||||
'outputDirectory' => '404.html',
|
||||
'fallbackFile' => '404.html',
|
||||
'installCommand' => 'npm install',
|
||||
'buildCommand' => 'npm run build',
|
||||
'outputDirectory' => './src/.vuepress/dist',
|
||||
@@ -298,7 +295,7 @@ return [
|
||||
'frameworks' => [
|
||||
getFramework('REACT', [
|
||||
'providerRootDirectory' => './react/docusaurus',
|
||||
'outputDirectory' => '404.html',
|
||||
'fallbackFile' => '404.html',
|
||||
'installCommand' => 'npm install',
|
||||
'buildCommand' => 'npm run build',
|
||||
'outputDirectory' => './build',
|
||||
@@ -1490,13 +1487,13 @@ return [
|
||||
]
|
||||
],
|
||||
[
|
||||
'key' => 'crm-dashboard-react-admin',
|
||||
'name' => 'CRM dashboard with React Admin',
|
||||
'tagline' => 'A React-based admin dashboard template with CRM features.',
|
||||
'key' => 'dashboard-react-admin',
|
||||
'name' => 'E-commerce dashboard with React Admin',
|
||||
'tagline' => 'A React-based admin dashboard template with e-commerce features.',
|
||||
'score' => 4, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
|
||||
'useCases' => [SiteUseCases::DASHBOARD],
|
||||
'screenshotDark' => $url . '/images/sites/templates/crm-dashboard-react-admin-dark.png',
|
||||
'screenshotLight' => $url . '/images/sites/templates/crm-dashboard-react-admin-light.png',
|
||||
'useCases' => [SiteUseCases::DASHBOARD, SiteUseCases::ECOMMERCE],
|
||||
'screenshotDark' => $url . '/images/sites/templates/dashboard-react-admin-dark.png',
|
||||
'screenshotLight' => $url . '/images/sites/templates/dashboard-react-admin-light.png',
|
||||
'frameworks' => [
|
||||
getFramework('REACT', [
|
||||
'providerRootDirectory' => './react/react-admin',
|
||||
|
||||
@@ -872,18 +872,18 @@ return [
|
||||
],
|
||||
[
|
||||
'name' => '_APP_FUNCTIONS_BUILD_TIMEOUT',
|
||||
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 900 seconds.',
|
||||
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 2700 seconds.',
|
||||
'introduction' => '0.13.0',
|
||||
'default' => '900',
|
||||
'default' => '2700',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_COMPUTE_BUILD_TIMEOUT',
|
||||
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 900 seconds.',
|
||||
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 2700 seconds.',
|
||||
'introduction' => '1.7.0',
|
||||
'default' => '900',
|
||||
'default' => '2700',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
|
||||
+421
-323
File diff suppressed because it is too large
Load Diff
@@ -231,7 +231,7 @@ function execute(
|
||||
$validations = GraphQL::getStandardValidationRules();
|
||||
|
||||
if (System::getEnv('_APP_GRAPHQL_INTROSPECTION', 'enabled') === 'disabled') {
|
||||
$validations[] = new DisableIntrospection();
|
||||
$validations[] = new DisableIntrospection(DisableIntrospection::ENABLED);
|
||||
}
|
||||
|
||||
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') {
|
||||
|
||||
@@ -231,6 +231,7 @@ Http::get('/v1/locale/continents')
|
||||
->inject('locale')
|
||||
->action(function (Response $response, Locale $locale) {
|
||||
$list = array_keys(Config::getParam('locale-continents'));
|
||||
$output = [];
|
||||
|
||||
foreach ($list as $value) {
|
||||
$output[] = new Document([
|
||||
|
||||
@@ -482,7 +482,6 @@ Http::post('/v1/messaging/providers/msg91')
|
||||
$enabled === true
|
||||
&& \array_key_exists('senderId', $credentials)
|
||||
&& \array_key_exists('authKey', $credentials)
|
||||
&& \array_key_exists('from', $options)
|
||||
) {
|
||||
$enabled = true;
|
||||
} else {
|
||||
@@ -1180,6 +1179,7 @@ Http::get('/v1/messaging/providers/:providerId/logs')
|
||||
'userEmail' => $log['data']['userEmail'] ?? null,
|
||||
'userName' => $log['data']['userName'] ?? null,
|
||||
'mode' => $log['data']['mode'] ?? null,
|
||||
'userType' => $log['data']['userType'] ?? null,
|
||||
'ip' => $log['ip'],
|
||||
'time' => $log['time'],
|
||||
'osCode' => $os['osCode'],
|
||||
@@ -2585,6 +2585,7 @@ Http::get('/v1/messaging/topics/:topicId/logs')
|
||||
'userEmail' => $log['data']['userEmail'] ?? null,
|
||||
'userName' => $log['data']['userName'] ?? null,
|
||||
'mode' => $log['data']['mode'] ?? null,
|
||||
'userType' => $log['data']['userType'] ?? null,
|
||||
'ip' => $log['ip'],
|
||||
'time' => $log['time'],
|
||||
'osCode' => $os['osCode'],
|
||||
@@ -3000,6 +3001,7 @@ Http::get('/v1/messaging/subscribers/:subscriberId/logs')
|
||||
'userEmail' => $log['data']['userEmail'] ?? null,
|
||||
'userName' => $log['data']['userName'] ?? null,
|
||||
'mode' => $log['data']['mode'] ?? null,
|
||||
'userType' => $log['data']['userType'] ?? null,
|
||||
'ip' => $log['ip'],
|
||||
'time' => $log['time'],
|
||||
'osCode' => $os['osCode'],
|
||||
@@ -3204,10 +3206,6 @@ Http::post('/v1/messaging/messages/email')
|
||||
throw new Exception(Exception::MESSAGE_MISSING_TARGET);
|
||||
}
|
||||
|
||||
if ($status === MessageStatus::SCHEDULED && \is_null($scheduledAt)) {
|
||||
throw new Exception(Exception::MESSAGE_MISSING_SCHEDULE);
|
||||
}
|
||||
|
||||
$mergedTargets = \array_merge($targets, $cc, $bcc);
|
||||
|
||||
if (!empty($mergedTargets)) {
|
||||
@@ -3383,10 +3381,6 @@ Http::post('/v1/messaging/messages/sms')
|
||||
throw new Exception(Exception::MESSAGE_MISSING_TARGET);
|
||||
}
|
||||
|
||||
if ($status === MessageStatus::SCHEDULED && \is_null($scheduledAt)) {
|
||||
throw new Exception(Exception::MESSAGE_MISSING_SCHEDULE);
|
||||
}
|
||||
|
||||
if (!empty($targets)) {
|
||||
$foundTargets = $dbForProject->find('targets', [
|
||||
Query::equal('$id', $targets),
|
||||
@@ -3524,10 +3518,6 @@ Http::post('/v1/messaging/messages/push')
|
||||
throw new Exception(Exception::MESSAGE_MISSING_TARGET);
|
||||
}
|
||||
|
||||
if ($status === MessageStatus::SCHEDULED && \is_null($scheduledAt)) {
|
||||
throw new Exception(Exception::MESSAGE_MISSING_SCHEDULE);
|
||||
}
|
||||
|
||||
if (!empty($targets)) {
|
||||
$foundTargets = $dbForProject->find('targets', [
|
||||
Query::equal('$id', $targets),
|
||||
@@ -3566,7 +3556,7 @@ Http::post('/v1/messaging/messages/push')
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$endpoint = "$protocol://{$platform['apiHostname']}/v1";
|
||||
|
||||
$scheduleTime = $currentScheduledAt ?? $scheduledAt;
|
||||
$scheduleTime = $scheduledAt;
|
||||
if (!\is_null($scheduleTime)) {
|
||||
$expiry = (new \DateTime($scheduleTime))->add(new \DateInterval('P15D'))->format('U');
|
||||
} else {
|
||||
@@ -3813,6 +3803,7 @@ Http::get('/v1/messaging/messages/:messageId/logs')
|
||||
'userEmail' => $log['data']['userEmail'] ?? null,
|
||||
'userName' => $log['data']['userName'] ?? null,
|
||||
'mode' => $log['data']['mode'] ?? null,
|
||||
'userType' => $log['data']['userType'] ?? null,
|
||||
'ip' => $log['ip'],
|
||||
'time' => $log['time'],
|
||||
'osCode' => $os['osCode'],
|
||||
@@ -4656,7 +4647,7 @@ Http::delete('/v1/messaging/messages/:messageId')
|
||||
if (!empty($scheduleId)) {
|
||||
try {
|
||||
$dbForPlatform->deleteDocument('schedules', $scheduleId);
|
||||
} catch (Exception) {
|
||||
} catch (\Throwable) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Message\Migration as MigrationMessage;
|
||||
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\OpenSSL\OpenSSL;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -50,7 +51,8 @@ function getDatabaseTransferResourceServices(string $databaseType)
|
||||
DATABASE_TYPE_LEGACY,
|
||||
DATABASE_TYPE_TABLESDB => Transfer::GROUP_DATABASES_TABLES_DB,
|
||||
DATABASE_TYPE_VECTORSDB => Transfer::GROUP_DATABASES_VECTOR_DB,
|
||||
DATABASE_TYPE_DOCUMENTSDB => Transfer::GROUP_DATABASES_DOCUMENTS_DB
|
||||
DATABASE_TYPE_DOCUMENTSDB => Transfer::GROUP_DATABASES_DOCUMENTS_DB,
|
||||
default => throw new \LogicException('Unknown database type: ' . $databaseType),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,10 +92,9 @@ Http::post('/v1/migrations/appwrite')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (array $resources, string $endpoint, string $projectId, string $apiKey, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (array $resources, string $endpoint, string $projectId, string $apiKey, Response $response, Database $dbForProject, Document $project, array $platform, Event $queueForEvents, MigrationPublisher $publisherForMigrations) {
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
@@ -114,12 +115,11 @@ Http::post('/v1/migrations/appwrite')
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -151,10 +151,9 @@ Http::post('/v1/migrations/firebase')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (array $resources, string $serviceAccount, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (array $resources, string $serviceAccount, Response $response, Database $dbForProject, Document $project, array $platform, Event $queueForEvents, MigrationPublisher $publisherForMigrations) {
|
||||
$serviceAccountData = json_decode($serviceAccount, true);
|
||||
|
||||
if (empty($serviceAccountData)) {
|
||||
@@ -183,12 +182,11 @@ Http::post('/v1/migrations/firebase')
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -225,10 +223,9 @@ Http::post('/v1/migrations/supabase')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Event $queueForEvents, MigrationPublisher $publisherForMigrations) {
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
@@ -252,12 +249,11 @@ Http::post('/v1/migrations/supabase')
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -295,10 +291,9 @@ Http::post('/v1/migrations/nhost')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Event $queueForEvents, MigrationPublisher $publisherForMigrations) {
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
@@ -323,12 +318,11 @@ Http::post('/v1/migrations/nhost')
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -368,7 +362,7 @@ Http::post('/v1/migrations/csv/imports')
|
||||
->inject('deviceForFiles')
|
||||
->inject('deviceForMigrations')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (
|
||||
string $bucketId,
|
||||
string $fileId,
|
||||
@@ -383,7 +377,7 @@ Http::post('/v1/migrations/csv/imports')
|
||||
Device $deviceForFiles,
|
||||
Device $deviceForMigrations,
|
||||
Event $queueForEvents,
|
||||
Migration $queueForMigrations
|
||||
MigrationPublisher $publisherForMigrations
|
||||
) {
|
||||
$bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
|
||||
if ($internalFile) {
|
||||
@@ -479,11 +473,10 @@ Http::post('/v1/migrations/csv/imports')
|
||||
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -526,7 +519,7 @@ Http::post('/v1/migrations/csv/exports')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (
|
||||
string $resourceId,
|
||||
string $filename,
|
||||
@@ -545,7 +538,7 @@ Http::post('/v1/migrations/csv/exports')
|
||||
Document $project,
|
||||
array $platform,
|
||||
Event $queueForEvents,
|
||||
Migration $queueForMigrations
|
||||
MigrationPublisher $publisherForMigrations
|
||||
) {
|
||||
try {
|
||||
$parsedQueries = Query::parseQueries($queries);
|
||||
@@ -630,11 +623,11 @@ Http::post('/v1/migrations/csv/exports')
|
||||
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -673,7 +666,7 @@ Http::post('/v1/migrations/json/imports')
|
||||
->inject('deviceForFiles')
|
||||
->inject('deviceForMigrations')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (
|
||||
string $bucketId,
|
||||
string $fileId,
|
||||
@@ -688,7 +681,7 @@ Http::post('/v1/migrations/json/imports')
|
||||
Device $deviceForFiles,
|
||||
Device $deviceForMigrations,
|
||||
Event $queueForEvents,
|
||||
Migration $queueForMigrations
|
||||
MigrationPublisher $publisherForMigrations
|
||||
) {
|
||||
$bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
|
||||
if ($internalFile) {
|
||||
@@ -783,11 +776,11 @@ Http::post('/v1/migrations/json/imports')
|
||||
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -826,7 +819,7 @@ Http::post('/v1/migrations/json/exports')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (
|
||||
string $resourceId,
|
||||
string $filename,
|
||||
@@ -841,7 +834,7 @@ Http::post('/v1/migrations/json/exports')
|
||||
Document $project,
|
||||
array $platform,
|
||||
Event $queueForEvents,
|
||||
Migration $queueForMigrations
|
||||
MigrationPublisher $publisherForMigrations
|
||||
) {
|
||||
try {
|
||||
$parsedQueries = Query::parseQueries($queries);
|
||||
@@ -915,11 +908,11 @@ Http::post('/v1/migrations/json/exports')
|
||||
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -1216,9 +1209,8 @@ Http::patch('/v1/migrations/:migrationId')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('user')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (string $migrationId, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Migration $queueForMigrations) {
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (string $migrationId, Response $response, Database $dbForProject, Document $project, array $platform, MigrationPublisher $publisherForMigrations) {
|
||||
$migration = $dbForProject->getDocument('migrations', $migrationId);
|
||||
|
||||
if ($migration->isEmpty()) {
|
||||
@@ -1234,12 +1226,11 @@ Http::patch('/v1/migrations/:migrationId')
|
||||
->setAttribute('dateUpdated', \time());
|
||||
|
||||
// Trigger Migration
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
@@ -113,11 +113,12 @@ Http::get('/v1/project/usage')
|
||||
$factor = match ($period) {
|
||||
'1h' => 3600,
|
||||
'1d' => 86400,
|
||||
default => throw new \LogicException('Unsupported period: ' . $period),
|
||||
};
|
||||
|
||||
$limit = match ($period) {
|
||||
'1h' => (new DateTime($startDate))->diff(new DateTime($endDate))->days * 24,
|
||||
'1d' => (new DateTime($startDate))->diff(new DateTime($endDate))->days
|
||||
'1d' => (new DateTime($startDate))->diff(new DateTime($endDate))->days,
|
||||
};
|
||||
|
||||
$format = match ($period) {
|
||||
|
||||
+10
-1747
File diff suppressed because it is too large
Load Diff
+111
-51
@@ -73,7 +73,7 @@ use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
/** TODO: Remove function when we move to using utopia/platform */
|
||||
function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, ?string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
|
||||
function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, ?string $name, Document $project, Database $dbForProject, Hooks $hooks, array $plan): Document
|
||||
{
|
||||
$name = $name ?? '';
|
||||
$plaintextPassword = $password;
|
||||
@@ -110,11 +110,39 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
|
||||
}
|
||||
}
|
||||
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => null,
|
||||
'emailIsCanonical' => null,
|
||||
'emailIsCorporate' => null,
|
||||
'emailIsDisposable' => null,
|
||||
'emailIsFree' => null,
|
||||
];
|
||||
|
||||
try {
|
||||
$emailCanonical = new Email($email);
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
$parsedEmail = new Email($email ?? '');
|
||||
$canonical = $parsedEmail->getCanonical();
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => $canonical,
|
||||
'emailIsCanonical' => $parsedEmail->get() === $canonical,
|
||||
'emailIsCorporate' => $parsedEmail->isCorporate(),
|
||||
'emailIsDisposable' => $parsedEmail->isDisposable(),
|
||||
'emailIsFree' => $parsedEmail->isFree(),
|
||||
];
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
|
||||
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
|
||||
}
|
||||
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
|
||||
}
|
||||
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
|
||||
throw new Exception(Exception::USER_EMAIL_FREE);
|
||||
}
|
||||
|
||||
$hashedPassword = null;
|
||||
|
||||
$isHashed = !$hash instanceof Plaintext;
|
||||
@@ -159,11 +187,11 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
|
||||
'tokens' => null,
|
||||
'memberships' => null,
|
||||
'search' => implode(' ', [$userId, $email, $phone, $name]),
|
||||
'emailCanonical' => $emailCanonical?->getCanonical(),
|
||||
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
|
||||
'emailIsCorporate' => $emailCanonical?->isCorporate(),
|
||||
'emailIsDisposable' => $emailCanonical?->isDisposable(),
|
||||
'emailIsFree' => $emailCanonical?->isFree(),
|
||||
'emailCanonical' => $emailMetadata['emailCanonical'],
|
||||
'emailIsCanonical' => $emailMetadata['emailIsCanonical'],
|
||||
'emailIsCorporate' => $emailMetadata['emailIsCorporate'],
|
||||
'emailIsDisposable' => $emailMetadata['emailIsDisposable'],
|
||||
'emailIsFree' => $emailMetadata['emailIsFree'],
|
||||
]);
|
||||
|
||||
if (!$isHashed && !empty($password)) {
|
||||
@@ -256,10 +284,11 @@ Http::post('/v1/users')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->inject('plan')
|
||||
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
|
||||
$plaintext = new Plaintext();
|
||||
|
||||
$user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks, $plan);
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($user, Response::MODEL_USER);
|
||||
@@ -292,11 +321,12 @@ Http::post('/v1/users/bcrypt')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->inject('plan')
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
|
||||
$bcrypt = new Bcrypt();
|
||||
$bcrypt->setCost(8); // Default cost
|
||||
|
||||
$user = createUser($bcrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser($bcrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -330,10 +360,11 @@ Http::post('/v1/users/md5')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->inject('plan')
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
|
||||
$md5 = new MD5();
|
||||
|
||||
$user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -367,10 +398,11 @@ Http::post('/v1/users/argon2')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->inject('plan')
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
|
||||
$argon2 = new Argon2();
|
||||
|
||||
$user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -405,13 +437,14 @@ Http::post('/v1/users/sha')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordVersion, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->inject('plan')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordVersion, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
|
||||
$sha = new Sha();
|
||||
if (!empty($passwordVersion)) {
|
||||
$sha->setVersion($passwordVersion);
|
||||
}
|
||||
|
||||
$user = createUser($sha, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser($sha, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -445,10 +478,11 @@ Http::post('/v1/users/phpass')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->inject('plan')
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
|
||||
$phpass = new PHPass();
|
||||
|
||||
$user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -487,7 +521,8 @@ Http::post('/v1/users/scrypt')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->inject('plan')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
|
||||
$scrypt = new Scrypt();
|
||||
$scrypt
|
||||
->setSalt($passwordSalt)
|
||||
@@ -496,7 +531,7 @@ Http::post('/v1/users/scrypt')
|
||||
->setParallelCost($passwordParallel)
|
||||
->setLength($passwordLength);
|
||||
|
||||
$user = createUser($scrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser($scrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -533,14 +568,15 @@ Http::post('/v1/users/scrypt-modified')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->inject('plan')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
|
||||
$scryptModified = new ScryptModified();
|
||||
$scryptModified
|
||||
->setSalt($passwordSalt)
|
||||
->setSaltSeparator($passwordSaltSeparator)
|
||||
->setSignerKey($passwordSignerKey);
|
||||
|
||||
$user = createUser($scryptModified, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser($scryptModified, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -972,6 +1008,8 @@ Http::get('/v1/users/:userId/logs')
|
||||
'userId' => ID::custom($log['data']['userId']),
|
||||
'userEmail' => $log['data']['userEmail'] ?? null,
|
||||
'userName' => $log['data']['userName'] ?? null,
|
||||
'mode' => $log['data']['mode'] ?? null,
|
||||
'userType' => $log['data']['userType'] ?? null,
|
||||
'ip' => $log['ip'],
|
||||
'time' => $log['time'],
|
||||
'osCode' => $os['osCode'],
|
||||
@@ -1470,8 +1508,10 @@ Http::patch('/v1/users/:userId/email')
|
||||
->param('email', '', new EmailValidator(allowEmpty: true), 'User email.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('plan')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $email, Response $response, Database $dbForProject, Event $queueForEvents) {
|
||||
->action(function (string $userId, string $email, Response $response, Database $dbForProject, Document $project, array $plan, Event $queueForEvents) {
|
||||
|
||||
$user = $dbForProject->getDocument('users', $userId);
|
||||
|
||||
@@ -1495,27 +1535,54 @@ Http::patch('/v1/users/:userId/email')
|
||||
Query::equal('identifier', [$email]),
|
||||
]);
|
||||
|
||||
if ($target instanceof Document && !$target->isEmpty()) {
|
||||
if (!$target->isEmpty()) {
|
||||
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
$oldEmail = $user->getAttribute('email');
|
||||
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => null,
|
||||
'emailIsCanonical' => null,
|
||||
'emailIsCorporate' => null,
|
||||
'emailIsDisposable' => null,
|
||||
'emailIsFree' => null,
|
||||
];
|
||||
|
||||
try {
|
||||
$emailCanonical = new Email($email);
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
$parsedEmail = new Email($email);
|
||||
$canonical = $parsedEmail->getCanonical();
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => $canonical,
|
||||
'emailIsCanonical' => $parsedEmail->get() === $canonical,
|
||||
'emailIsCorporate' => $parsedEmail->isCorporate(),
|
||||
'emailIsDisposable' => $parsedEmail->isDisposable(),
|
||||
'emailIsFree' => $parsedEmail->isFree(),
|
||||
];
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
|
||||
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
|
||||
}
|
||||
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
|
||||
}
|
||||
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
|
||||
throw new Exception(Exception::USER_EMAIL_FREE);
|
||||
}
|
||||
|
||||
$user
|
||||
->setAttribute('email', $email)
|
||||
->setAttribute('emailVerification', false)
|
||||
->setAttribute('emailCanonical', $emailCanonical?->getCanonical())
|
||||
->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported())
|
||||
->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate())
|
||||
->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable())
|
||||
->setAttribute('emailIsFree', $emailCanonical?->isFree())
|
||||
->setAttribute('emailCanonical', $emailMetadata['emailCanonical'])
|
||||
->setAttribute('emailIsCanonical', $emailMetadata['emailIsCanonical'])
|
||||
->setAttribute('emailIsCorporate', $emailMetadata['emailIsCorporate'])
|
||||
->setAttribute('emailIsDisposable', $emailMetadata['emailIsDisposable'])
|
||||
->setAttribute('emailIsFree', $emailMetadata['emailIsFree'])
|
||||
;
|
||||
|
||||
try {
|
||||
@@ -1528,9 +1595,6 @@ Http::patch('/v1/users/:userId/email')
|
||||
'emailIsDisposable' => $user->getAttribute('emailIsDisposable'),
|
||||
'emailIsFree' => $user->getAttribute('emailIsFree'),
|
||||
]));
|
||||
/**
|
||||
* @var Document $oldTarget
|
||||
*/
|
||||
$oldTarget = $user->find('identifier', $oldEmail, 'targets');
|
||||
|
||||
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
|
||||
@@ -1614,7 +1678,7 @@ Http::patch('/v1/users/:userId/phone')
|
||||
Query::equal('identifier', [$number]),
|
||||
]);
|
||||
|
||||
if ($target instanceof Document && !$target->isEmpty()) {
|
||||
if (!$target->isEmpty()) {
|
||||
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
|
||||
}
|
||||
}
|
||||
@@ -1624,9 +1688,6 @@ Http::patch('/v1/users/:userId/phone')
|
||||
'phone' => $phoneValue,
|
||||
'phoneVerification' => $user->getAttribute('phoneVerification'),
|
||||
]));
|
||||
/**
|
||||
* @var Document $oldTarget
|
||||
*/
|
||||
$oldTarget = $user->find('identifier', $oldPhone, 'targets');
|
||||
|
||||
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
|
||||
@@ -2185,8 +2246,8 @@ Http::delete('/v1/users/:userId/mfa/authenticators/:type')
|
||||
->label('event', 'users.[userId].delete.mfa')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'user.update')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('audits.userId', '{response.$id}')
|
||||
->label('audits.resource', 'user/{request.userId}')
|
||||
->label('audits.userId', '{request.userId}')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk', [
|
||||
new Method(
|
||||
@@ -2337,9 +2398,8 @@ Http::post('/v1/users/:userId/sessions')
|
||||
->setParam('sessionId', $session->getId())
|
||||
->setPayload($response->output($session, Response::MODEL_SESSION));
|
||||
|
||||
return $response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($session, Response::MODEL_SESSION);
|
||||
$response->setStatusCode(Response::STATUS_CODE_CREATED);
|
||||
$response->dynamic($session, Response::MODEL_SESSION);
|
||||
});
|
||||
|
||||
Http::post('/v1/users/:userId/tokens')
|
||||
@@ -2402,9 +2462,8 @@ Http::post('/v1/users/:userId/tokens')
|
||||
->setParam('tokenId', $token->getId())
|
||||
->setPayload($response->output($token, Response::MODEL_TOKEN));
|
||||
|
||||
return $response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($token, Response::MODEL_TOKEN);
|
||||
$response->setStatusCode(Response::STATUS_CODE_CREATED);
|
||||
$response->dynamic($token, Response::MODEL_TOKEN);
|
||||
});
|
||||
|
||||
Http::delete('/v1/users/:userId/sessions/:sessionId')
|
||||
@@ -2658,7 +2717,7 @@ Http::delete('/v1/users/identities/:identityId')
|
||||
->setParam('identityId', $identity->getId())
|
||||
->setPayload($response->output($identity, Response::MODEL_IDENTITY));
|
||||
|
||||
return $response->noContent();
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
Http::post('/v1/users/:userId/jwts')
|
||||
@@ -2777,6 +2836,7 @@ Http::get('/v1/users/usage')
|
||||
$format = match ($days['period']) {
|
||||
'1h' => 'Y-m-d\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
default => throw new \LogicException('Unsupported period: ' . $days['period']),
|
||||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
|
||||
+94
-97
@@ -7,9 +7,9 @@ use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Auth\Key;
|
||||
use Appwrite\Bus\Events\ExecutionCompleted;
|
||||
use Appwrite\Bus\Events\RequestCompleted;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Delete as DeleteEvent;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Publisher\Certificate;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Appwrite\Network\Cors;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
@@ -25,6 +25,8 @@ use Appwrite\Utopia\Request\Filters\V18 as RequestV18;
|
||||
use Appwrite\Utopia\Request\Filters\V19 as RequestV19;
|
||||
use Appwrite\Utopia\Request\Filters\V20 as RequestV20;
|
||||
use Appwrite\Utopia\Request\Filters\V21 as RequestV21;
|
||||
use Appwrite\Utopia\Request\Filters\V22 as RequestV22;
|
||||
use Appwrite\Utopia\Request\Filters\V23 as RequestV23;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
|
||||
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
|
||||
@@ -32,6 +34,8 @@ use Appwrite\Utopia\Response\Filters\V18 as ResponseV18;
|
||||
use Appwrite\Utopia\Response\Filters\V19 as ResponseV19;
|
||||
use Appwrite\Utopia\Response\Filters\V20 as ResponseV20;
|
||||
use Appwrite\Utopia\Response\Filters\V21 as ResponseV21;
|
||||
use Appwrite\Utopia\Response\Filters\V22 as ResponseV22;
|
||||
use Appwrite\Utopia\Response\Filters\V23 as ResponseV23;
|
||||
use Appwrite\Utopia\View;
|
||||
use Executor\Executor;
|
||||
use MaxMind\Db\Reader;
|
||||
@@ -61,13 +65,11 @@ use Utopia\System\System;
|
||||
use Utopia\Validator;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
Config::setParam('domainVerification', false);
|
||||
Config::setParam('cookieDomain', 'localhost');
|
||||
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
|
||||
|
||||
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
|
||||
{
|
||||
$host = $request->getHostname() ?? '';
|
||||
$host = $request->getHostname();
|
||||
if (!empty($previewHostname)) {
|
||||
$host = $previewHostname;
|
||||
}
|
||||
@@ -166,14 +168,14 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
if ($request->getMethod() !== Request::METHOD_GET) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.', view: $errorView);
|
||||
}
|
||||
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
|
||||
$response->redirect('https://' . $request->getHostname() . $request->getURI());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var Database $dbForProject */
|
||||
$dbForProject = $getProjectDB($project);
|
||||
|
||||
/** @var Document $deployment */
|
||||
if (!empty($rule->getAttribute('deploymentId', ''))) {
|
||||
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId')));
|
||||
} else {
|
||||
@@ -200,12 +202,6 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $activeDeploymentId));
|
||||
}
|
||||
|
||||
if ($deployment->getAttribute('resourceType', '') === 'functions') {
|
||||
$type = 'function';
|
||||
} elseif ($deployment->getAttribute('resourceType', '') === 'sites') {
|
||||
$type = 'site';
|
||||
}
|
||||
|
||||
if ($deployment->isEmpty()) {
|
||||
$resourceType = $rule->getAttribute('deploymentResourceType', '');
|
||||
$resourceId = $rule->getAttribute('deploymentResourceId', '');
|
||||
@@ -215,6 +211,14 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
if ($deployment->getAttribute('resourceType', '') === 'functions') {
|
||||
$type = 'function';
|
||||
} elseif ($deployment->getAttribute('resourceType', '') === 'sites') {
|
||||
$type = 'site';
|
||||
} else {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown deployment resource type', view: $errorView);
|
||||
}
|
||||
|
||||
$resource = $type === 'function' ?
|
||||
$authorization->skip(fn () => $dbForProject->getDocument('functions', $deployment->getAttribute('resourceId', ''))) :
|
||||
$authorization->skip(fn () => $dbForProject->getDocument('sites', $deployment->getAttribute('resourceId', '')));
|
||||
@@ -244,6 +248,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
if ($isPreview && $requirePreview) {
|
||||
$cookie = $request->getCookie(COOKIE_NAME_PREVIEW, '');
|
||||
$authorized = false;
|
||||
$user = new Document();
|
||||
|
||||
// Security checks to mark authorized true
|
||||
if (!empty($cookie)) {
|
||||
@@ -273,7 +278,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
|
||||
$membershipExists = false;
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
|
||||
if (!$project->isEmpty() && isset($user)) {
|
||||
if (!$project->isEmpty() && !$user->isEmpty()) {
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
$membership = $user->find('teamId', $teamId, 'memberships');
|
||||
if (!empty($membership)) {
|
||||
@@ -301,13 +306,13 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
}
|
||||
}
|
||||
|
||||
$body = $swooleRequest->getContent() ?? '';
|
||||
$body = $swooleRequest->getContent() ?: '';
|
||||
$method = $swooleRequest->server['request_method'];
|
||||
|
||||
$requestHeaders = $request->getHeaders();
|
||||
|
||||
if ($resource->isEmpty() || !$resource->getAttribute('enabled')) {
|
||||
if ($type === 'functions') {
|
||||
if ($type === 'function') {
|
||||
throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND, view: $errorView);
|
||||
} else {
|
||||
throw new AppwriteException(AppwriteException::SITE_NOT_FOUND, view: $errorView);
|
||||
@@ -329,7 +334,6 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
$runtime = match ($type) {
|
||||
'function' => $runtimes[$resource->getAttribute('runtime')] ?? null,
|
||||
'site' => $runtimes[$resource->getAttribute('buildRuntime')] ?? null,
|
||||
default => null
|
||||
};
|
||||
|
||||
// Static site enforced runtime
|
||||
@@ -379,7 +383,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
$executionId = ID::unique();
|
||||
|
||||
$headers = \array_merge([], $requestHeaders);
|
||||
$headers['x-appwrite-execution-id'] = $executionId ?? '';
|
||||
$headers['x-appwrite-execution-id'] = $executionId;
|
||||
$headers['x-appwrite-user-id'] = '';
|
||||
$headers['x-appwrite-country-code'] = '';
|
||||
$headers['x-appwrite-continent-code'] = '';
|
||||
@@ -458,10 +462,10 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
// V2 vars
|
||||
if ($version === 'v2') {
|
||||
$vars = \array_merge($vars, [
|
||||
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
|
||||
'APPWRITE_FUNCTION_DATA' => $body ?? '',
|
||||
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
|
||||
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
|
||||
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'],
|
||||
'APPWRITE_FUNCTION_DATA' => $body,
|
||||
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'],
|
||||
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt']
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -529,6 +533,11 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
}
|
||||
|
||||
/** Execute function */
|
||||
$executionResponse = [
|
||||
'headers' => [],
|
||||
'body' => '',
|
||||
];
|
||||
|
||||
try {
|
||||
$version = match ($type) {
|
||||
'function' => $resource->getAttribute('version', 'v2'),
|
||||
@@ -672,9 +681,8 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
|
||||
if (\is_string($logs) && \strlen($logs) > $maxLogLength) {
|
||||
$warningMessage = "[WARNING] Logs truncated. The output exceeded {$maxLogLength} characters.\n";
|
||||
$warningLength = \strlen($warningMessage);
|
||||
$maxContentLength = max(0, $maxLogLength - $warningLength);
|
||||
$logs = $warningMessage . ($maxContentLength > 0 ? \substr($logs, -$maxContentLength) : '');
|
||||
$maxContentLength = $maxLogLength - \strlen($warningMessage);
|
||||
$logs = $warningMessage . \substr($logs, -$maxContentLength);
|
||||
}
|
||||
|
||||
// Truncate errors if they exceed the limit
|
||||
@@ -683,9 +691,8 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
|
||||
if (\is_string($errors) && \strlen($errors) > $maxErrorLength) {
|
||||
$warningMessage = "[WARNING] Errors truncated. The output exceeded {$maxErrorLength} characters.\n";
|
||||
$warningLength = \strlen($warningMessage);
|
||||
$maxContentLength = max(0, $maxErrorLength - $warningLength);
|
||||
$errors = $warningMessage . ($maxContentLength > 0 ? \substr($errors, -$maxContentLength) : '');
|
||||
$maxContentLength = $maxErrorLength - \strlen($warningMessage);
|
||||
$errors = $warningMessage . \substr($errors, -$maxContentLength);
|
||||
}
|
||||
/** Update execution status */
|
||||
$status = $executionResponse['statusCode'] >= 500 ? 'failed' : 'completed';
|
||||
@@ -713,14 +720,12 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
throw $th;
|
||||
}
|
||||
} finally {
|
||||
if ($type === 'function' || $type === 'site') {
|
||||
$bus->dispatch(new ExecutionCompleted(
|
||||
execution: $execution->getArrayCopy(),
|
||||
project: $project->getArrayCopy(),
|
||||
spec: $spec,
|
||||
resource: $resource->getArrayCopy(),
|
||||
));
|
||||
}
|
||||
$bus->dispatch(new ExecutionCompleted(
|
||||
execution: $execution->getArrayCopy(),
|
||||
project: $project->getArrayCopy(),
|
||||
spec: $spec,
|
||||
resource: $resource->getArrayCopy(),
|
||||
));
|
||||
}
|
||||
|
||||
$execution->setAttribute('logs', '');
|
||||
@@ -734,7 +739,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
$execution->setAttribute('responseBody', $executionResponse['body'] ?? '');
|
||||
$execution->setAttribute('responseHeaders', $headers);
|
||||
|
||||
$body = $execution['responseBody'] ?? '';
|
||||
$body = $execution['responseBody'];
|
||||
|
||||
$contentType = 'text/plain';
|
||||
foreach ($executionResponse['headers'] as $name => $values) {
|
||||
@@ -748,11 +753,8 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
}
|
||||
|
||||
if (\is_array($values)) {
|
||||
$count = 0;
|
||||
foreach ($values as $value) {
|
||||
$override = $count === 0;
|
||||
$response->addHeader($name, $value, override: $override);
|
||||
$count++;
|
||||
$response->addHeader($name, $value);
|
||||
}
|
||||
} else {
|
||||
$response->addHeader($name, $values);
|
||||
@@ -849,7 +851,7 @@ Http::init()
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
$hostname = $request->getHostname() ?? '';
|
||||
$hostname = $request->getHostname();
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
// Only run Router when external domain
|
||||
if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
|
||||
@@ -862,12 +864,12 @@ Http::init()
|
||||
* Request format
|
||||
*/
|
||||
$route = $utopia->getRoute();
|
||||
Request::setRoute($route);
|
||||
$request->setRoute($route);
|
||||
|
||||
if ($route === null) {
|
||||
return $response
|
||||
->setStatusCode(404)
|
||||
->send('Not Found');
|
||||
$response->setStatusCode(404);
|
||||
$response->send('Not Found');
|
||||
return;
|
||||
}
|
||||
|
||||
$requestFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
|
||||
@@ -891,6 +893,12 @@ Http::init()
|
||||
if (version_compare($requestFormat, '1.9.0', '<')) {
|
||||
$request->addFilter(new RequestV21());
|
||||
}
|
||||
if (version_compare($requestFormat, '1.9.1', '<')) {
|
||||
$request->addFilter(new RequestV22());
|
||||
}
|
||||
if (version_compare($requestFormat, '1.9.2', '<')) {
|
||||
$request->addFilter(new RequestV23());
|
||||
}
|
||||
}
|
||||
|
||||
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
|
||||
@@ -898,40 +906,16 @@ Http::init()
|
||||
$locale->setDefault($localeParam);
|
||||
}
|
||||
|
||||
$origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST);
|
||||
$selfDomain = new Domain($request->getHostname());
|
||||
$endDomain = new Domain((string)$origin);
|
||||
Config::setParam(
|
||||
'domainVerification',
|
||||
($selfDomain->getRegisterable() === $endDomain->getRegisterable()) &&
|
||||
$endDomain->getRegisterable() !== ''
|
||||
);
|
||||
|
||||
$localHosts = ['localhost','localhost:'.$request->getPort()];
|
||||
|
||||
$migrationHost = System::getEnv('_APP_MIGRATION_HOST');
|
||||
if (!empty($migrationHost)) {
|
||||
// Treat the migration host like localhost because internal migration and
|
||||
// CI traffic may use it before a public domain is configured.
|
||||
$localHosts[] = $migrationHost;
|
||||
$localHosts[] = $migrationHost.':'.$request->getPort();
|
||||
}
|
||||
|
||||
$isLocalHost = in_array($request->getHostname(), $localHosts);
|
||||
$isIpAddress = filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false;
|
||||
|
||||
$isConsoleProject = $project->getAttribute('$id', '') === 'console';
|
||||
$isConsoleRootSession = System::getEnv('_APP_CONSOLE_ROOT_SESSION', 'disabled') === 'enabled';
|
||||
|
||||
Config::setParam(
|
||||
'cookieDomain',
|
||||
$isLocalHost || $isIpAddress
|
||||
? null
|
||||
: (
|
||||
$isConsoleProject && $isConsoleRootSession
|
||||
? '.' . $selfDomain->getRegisterable()
|
||||
: '.' . $request->getHostname()
|
||||
)
|
||||
);
|
||||
|
||||
$warnings = [];
|
||||
|
||||
/*
|
||||
@@ -939,6 +923,12 @@ Http::init()
|
||||
*/
|
||||
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
|
||||
if ($responseFormat) {
|
||||
if (version_compare($responseFormat, '1.9.2', '<')) {
|
||||
$response->addFilter(new ResponseV23());
|
||||
}
|
||||
if (version_compare($responseFormat, '1.9.1', '<')) {
|
||||
$response->addFilter(new ResponseV22());
|
||||
}
|
||||
if (version_compare($responseFormat, '1.9.0', '<')) {
|
||||
$response->addFilter(new ResponseV21());
|
||||
}
|
||||
@@ -973,7 +963,8 @@ Http::init()
|
||||
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.');
|
||||
}
|
||||
|
||||
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
|
||||
$response->redirect('https://' . $request->getHostname() . $request->getURI());
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1012,7 +1003,7 @@ Http::init()
|
||||
return;
|
||||
}
|
||||
$route = $request->getRoute();
|
||||
if ($route->getLabel('origin', false) === '*') {
|
||||
if ($route?->getLabel('origin', false) === '*') {
|
||||
return;
|
||||
}
|
||||
if (!$originValidator->isValid($origin)) {
|
||||
@@ -1028,11 +1019,11 @@ Http::init()
|
||||
->inject('request')
|
||||
->inject('console')
|
||||
->inject('dbForPlatform')
|
||||
->inject('queueForCertificates')
|
||||
->inject('publisherForCertificates')
|
||||
->inject('platform')
|
||||
->inject('authorization')
|
||||
->inject('certifiedDomains')
|
||||
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) {
|
||||
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $publisherForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) {
|
||||
$hostname = $request->getHostname();
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
|
||||
@@ -1058,7 +1049,7 @@ Http::init()
|
||||
}
|
||||
|
||||
// 4. Check/create rule (requires DB access)
|
||||
$authorization->skip(function () use ($dbForPlatform, $domain, $console, $queueForCertificates, $certifiedDomains) {
|
||||
$authorization->skip(function () use ($dbForPlatform, $domain, $console, $publisherForCertificates, $certifiedDomains) {
|
||||
try {
|
||||
// TODO: (@Meldiron) Remove after 1.7.x migration
|
||||
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
|
||||
@@ -1114,10 +1105,11 @@ Http::init()
|
||||
$dbForPlatform->createDocument('rules', $document);
|
||||
|
||||
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
|
||||
$queueForCertificates
|
||||
->setDomain($document)
|
||||
->setSkipRenewCheck(true)
|
||||
->trigger();
|
||||
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
|
||||
project: $console,
|
||||
domain: $document,
|
||||
skipRenewCheck: true,
|
||||
));
|
||||
} catch (Duplicate $e) {
|
||||
Console::info('Certificate already exists');
|
||||
} finally {
|
||||
@@ -1190,15 +1182,6 @@ Http::error()
|
||||
->inject('devKey')
|
||||
->inject('authorization')
|
||||
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) {
|
||||
$trace = $error->getTrace();
|
||||
|
||||
foreach (array_slice($trace, 0, 100) as $index => $traceEntry) {
|
||||
$file = isset($traceEntry['file']) ? $traceEntry['file'] : '[internal function]';
|
||||
$line = isset($traceEntry['line']) ? $traceEntry['line'] : '';
|
||||
$function = isset($traceEntry['function']) ? $traceEntry['function'] : '';
|
||||
Console::error("[$index] $file : $line -> $function()");
|
||||
}
|
||||
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
$route = $utopia->getRoute();
|
||||
$class = \get_class($error);
|
||||
@@ -1208,9 +1191,7 @@ Http::error()
|
||||
$line = $error->getLine();
|
||||
$trace = $error->getTrace();
|
||||
|
||||
if (php_sapi_name() === 'cli') {
|
||||
Span::error($error);
|
||||
}
|
||||
Span::error($error);
|
||||
|
||||
switch ($class) {
|
||||
case Utopia\Http\Exception::class:
|
||||
@@ -1452,6 +1433,7 @@ Http::error()
|
||||
case 402: // Error allowed publicly
|
||||
case 403: // Error allowed publicly
|
||||
case 404: // Error allowed publicly
|
||||
case 405: // Error allowed publicly
|
||||
case 408: // Error allowed publicly
|
||||
case 409: // Error allowed publicly
|
||||
case 412: // Error allowed publicly
|
||||
@@ -1486,6 +1468,21 @@ Http::error()
|
||||
'type' => $type,
|
||||
];
|
||||
|
||||
// Add CORS headers to error responses so browsers can read the error.
|
||||
// Wrapped in try-catch: if the error itself is a DB failure, resolving
|
||||
// the cors resource (which depends on rule -> DB) would cascade.
|
||||
// Uses override:true to avoid duplicate headers if init() already set them.
|
||||
try {
|
||||
$cors = $utopia->getResource('cors');
|
||||
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
|
||||
$response
|
||||
->removeHeader($name)
|
||||
->addHeader($name, $value);
|
||||
}
|
||||
} catch (Throwable) {
|
||||
// Degrade gracefully - error response without CORS is no worse than before.
|
||||
}
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
->addHeader('Expires', '0')
|
||||
@@ -1507,9 +1504,9 @@ Http::error()
|
||||
->setParam('development', Http::isDevelopment())
|
||||
->setParam('projectName', $project->getAttribute('name'))
|
||||
->setParam('projectURL', $project->getAttribute('url'))
|
||||
->setParam('message', $output['message'] ?? '')
|
||||
->setParam('type', $output['type'] ?? '')
|
||||
->setParam('code', $output['code'] ?? '')
|
||||
->setParam('message', $output['message'])
|
||||
->setParam('type', $output['type'])
|
||||
->setParam('code', $output['code'])
|
||||
->setParam('trace', $output['trace'] ?? [])
|
||||
->setParam('exception', $error);
|
||||
|
||||
@@ -1624,7 +1621,7 @@ Http::get('/.well-known/acme-challenge/*')
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND, 'Unknown path');
|
||||
}
|
||||
|
||||
if (!\substr($absolute, 0, \strlen($base)) === $base) {
|
||||
if (\substr($absolute, 0, \strlen($base)) !== $base) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_UNAUTHORIZED_SCOPE, 'Invalid path');
|
||||
}
|
||||
|
||||
@@ -1703,7 +1700,7 @@ Http::get('/_appwrite/authorize')
|
||||
->inject('previewHostname')
|
||||
->action(function (Request $request, Response $response, string $previewHostname) {
|
||||
|
||||
$host = $request->getHostname() ?? '';
|
||||
$host = $request->getHostname();
|
||||
if (!empty($previewHostname)) {
|
||||
$host = $previewHostname;
|
||||
}
|
||||
|
||||
+30
-28
@@ -244,36 +244,38 @@ Http::get('/v1/mock/github/callback')
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND, $error);
|
||||
}
|
||||
|
||||
if (!empty($providerInstallationId)) {
|
||||
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
|
||||
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
|
||||
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
|
||||
$owner = $github->getOwnerName($providerInstallationId) ?? '';
|
||||
|
||||
$projectInternalId = $project->getSequence();
|
||||
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
|
||||
$installation = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
Permission::read(Role::team(ID::custom($teamId))),
|
||||
Permission::update(Role::team(ID::custom($teamId), 'owner')),
|
||||
Permission::update(Role::team(ID::custom($teamId), 'developer')),
|
||||
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
|
||||
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
|
||||
],
|
||||
'providerInstallationId' => $providerInstallationId,
|
||||
'projectId' => $projectId,
|
||||
'projectInternalId' => $projectInternalId,
|
||||
'provider' => 'github',
|
||||
'organization' => $owner,
|
||||
'personal' => false
|
||||
]);
|
||||
|
||||
$installation = $dbForPlatform->createDocument('installations', $installation);
|
||||
if (empty($providerInstallationId)) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Missing provider installation ID');
|
||||
}
|
||||
|
||||
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
|
||||
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
|
||||
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
|
||||
$owner = $github->getOwnerName($providerInstallationId);
|
||||
|
||||
$projectInternalId = $project->getSequence();
|
||||
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
|
||||
$installation = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
Permission::read(Role::team(ID::custom($teamId))),
|
||||
Permission::update(Role::team(ID::custom($teamId), 'owner')),
|
||||
Permission::update(Role::team(ID::custom($teamId), 'developer')),
|
||||
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
|
||||
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
|
||||
],
|
||||
'providerInstallationId' => $providerInstallationId,
|
||||
'projectId' => $projectId,
|
||||
'projectInternalId' => $projectInternalId,
|
||||
'provider' => 'github',
|
||||
'organization' => $owner,
|
||||
'personal' => false
|
||||
]);
|
||||
|
||||
$installation = $dbForPlatform->createDocument('installations', $installation);
|
||||
|
||||
$response->json([
|
||||
'installationId' => $installation->getId(),
|
||||
]);
|
||||
|
||||
@@ -3,15 +3,17 @@
|
||||
use Appwrite\Auth\Key;
|
||||
use Appwrite\Auth\MFA\Type\TOTP;
|
||||
use Appwrite\Bus\Events\RequestCompleted;
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Context\Audit as AuditContext;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Message\Audit as AuditMessage;
|
||||
use Appwrite\Event\Message\Usage as UsageMessage;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Publisher\Audit;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Webhook;
|
||||
@@ -37,13 +39,14 @@ use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Authorization\Input;
|
||||
use Utopia\Database\Validator\Roles;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Span\Span;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter as Telemetry;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
$parseLabel = function (string $label, array $responsePayload, array $requestParams, User $user) {
|
||||
preg_match_all('/{(.*?)}/', $label, $matches);
|
||||
foreach ($matches[1] ?? [] as $pos => $match) {
|
||||
foreach ($matches[1] as $pos => $match) {
|
||||
$find = $matches[0][$pos];
|
||||
$parts = explode('.', $match);
|
||||
|
||||
@@ -51,8 +54,8 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, "The server encountered an error while parsing the label: $label. Please create an issue on GitHub to allow us to investigate further https://github.com/appwrite/appwrite/issues/new/choose");
|
||||
}
|
||||
|
||||
$namespace = $parts[0] ?? '';
|
||||
$replace = $parts[1] ?? '';
|
||||
$namespace = $parts[0];
|
||||
$replace = $parts[1];
|
||||
|
||||
$params = match ($namespace) {
|
||||
'user' => (array) $user,
|
||||
@@ -87,7 +90,7 @@ Http::init()
|
||||
->inject('request')
|
||||
->inject('dbForPlatform')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForAudits')
|
||||
->inject('auditContext')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('session')
|
||||
@@ -96,8 +99,11 @@ Http::init()
|
||||
->inject('team')
|
||||
->inject('apiKey')
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
|
||||
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
|
||||
$route = $utopia->getRoute();
|
||||
if ($route === null) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user authentication and session validation.
|
||||
@@ -183,13 +189,13 @@ Http::init()
|
||||
$user = new User([
|
||||
'$id' => '',
|
||||
'status' => true,
|
||||
'type' => ACTIVITY_TYPE_APP,
|
||||
'type' => ACTIVITY_TYPE_KEY_PROJECT,
|
||||
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
|
||||
'password' => '',
|
||||
'name' => $apiKey->getName(),
|
||||
]);
|
||||
|
||||
$queueForAudits->setUser($user);
|
||||
$auditContext->user = $user;
|
||||
}
|
||||
|
||||
// For standard keys, update last accessed time
|
||||
@@ -253,7 +259,13 @@ Http::init()
|
||||
}
|
||||
}
|
||||
|
||||
$queueForAudits->setUser($user);
|
||||
$userClone = clone $user;
|
||||
$userClone->setAttribute('type', match ($apiKey->getType()) {
|
||||
API_KEY_STANDARD => ACTIVITY_TYPE_KEY_PROJECT,
|
||||
API_KEY_ACCOUNT => ACTIVITY_TYPE_KEY_ACCOUNT,
|
||||
default => ACTIVITY_TYPE_KEY_ORGANIZATION,
|
||||
});
|
||||
$auditContext->user = $userClone;
|
||||
}
|
||||
|
||||
// Apply permission
|
||||
@@ -372,7 +384,7 @@ Http::init()
|
||||
}
|
||||
|
||||
// Step 6: Update project and user last activity
|
||||
if (! $project->isEmpty() && $project->getId() !== 'console') {
|
||||
if ($project->getId() !== 'console') {
|
||||
$accessedAt = $project->getAttribute('accessedAt', 0);
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
|
||||
@@ -402,9 +414,6 @@ Http::init()
|
||||
}
|
||||
|
||||
// Steps 7-9: Access Control - Method, Namespace and Scope Validation
|
||||
/**
|
||||
* @var ?Method $method
|
||||
*/
|
||||
$method = $route->getLabel('sdk', false);
|
||||
|
||||
// Take the first method if there's more than one,
|
||||
@@ -414,7 +423,7 @@ Http::init()
|
||||
}
|
||||
|
||||
if (! empty($method)) {
|
||||
$namespace = $method->getNamespace();
|
||||
$namespace = \strtolower($method->getNamespace());
|
||||
|
||||
if (
|
||||
array_key_exists($namespace, $project->getAttribute('services', []))
|
||||
@@ -425,6 +434,15 @@ Http::init()
|
||||
}
|
||||
}
|
||||
|
||||
// Step 8b: Check REST protocol status
|
||||
if (
|
||||
array_key_exists('rest', $project->getAttribute('apis', []))
|
||||
&& ! $project->getAttribute('apis', [])['rest']
|
||||
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
|
||||
// Step 9: Validate scope permissions
|
||||
$allowed = (array) $route->getLabel('scope', 'none');
|
||||
if (empty(\array_intersect($allowed, $scopes))) {
|
||||
@@ -466,7 +484,7 @@ Http::init()
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMessaging')
|
||||
->inject('queueForAudits')
|
||||
->inject('auditContext')
|
||||
->inject('queueForDeletes')
|
||||
->inject('queueForDatabase')
|
||||
->inject('queueForBuilds')
|
||||
@@ -483,12 +501,16 @@ Http::init()
|
||||
->inject('telemetry')
|
||||
->inject('platform')
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
|
||||
|
||||
$response->setUser($user);
|
||||
$request->setUser($user);
|
||||
|
||||
$route = $utopia->getRoute();
|
||||
if ($route === null) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$path = $route->getMatchedPath();
|
||||
$databaseType = match (true) {
|
||||
str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
|
||||
@@ -496,14 +518,6 @@ Http::init()
|
||||
default => '',
|
||||
};
|
||||
|
||||
if (
|
||||
array_key_exists('rest', $project->getAttribute('apis', []))
|
||||
&& ! $project->getAttribute('apis', [])['rest']
|
||||
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
|
||||
/*
|
||||
* Abuse Check
|
||||
*/
|
||||
@@ -580,20 +594,21 @@ Http::init()
|
||||
->setProject($project)
|
||||
->setUser($user);
|
||||
|
||||
$queueForAudits
|
||||
->setMode($mode)
|
||||
->setUserAgent($request->getUserAgent(''))
|
||||
->setIP($request->getIP())
|
||||
->setHostname($request->getHostname())
|
||||
->setEvent($route->getLabel('audits.event', ''))
|
||||
->setProject($project);
|
||||
$auditContext->mode = $mode;
|
||||
$auditContext->userAgent = $request->getUserAgent('');
|
||||
$auditContext->ip = $request->getIP();
|
||||
$auditContext->hostname = $request->getHostname();
|
||||
$auditContext->event = $route->getLabel('audits.event', '');
|
||||
$auditContext->project = $project;
|
||||
|
||||
/* If a session exists, use the user associated with the session */
|
||||
if (! $user->isEmpty()) {
|
||||
$userClone = clone $user;
|
||||
// $user doesn't support `type` and can cause unintended effects.
|
||||
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
|
||||
$queueForAudits->setUser($userClone);
|
||||
if (empty($user->getAttribute('type'))) {
|
||||
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
|
||||
}
|
||||
$auditContext->user = $userClone;
|
||||
}
|
||||
|
||||
/* Auto-set projects */
|
||||
@@ -617,6 +632,7 @@ Http::init()
|
||||
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$key = $request->cacheIdentifier();
|
||||
Span::add('storage.cache.key', $key);
|
||||
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
$cache = new Cache(
|
||||
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
|
||||
@@ -626,7 +642,7 @@ Http::init()
|
||||
|
||||
if (! empty($data) && ! $cacheLog->isEmpty()) {
|
||||
$parts = explode('/', $cacheLog->getAttribute('resourceType', ''));
|
||||
$type = $parts[0] ?? null;
|
||||
$type = $parts[0];
|
||||
|
||||
if ($type === 'bucket' && (! $isImageTransformation || ! $isDisabled)) {
|
||||
$bucketId = $parts[1] ?? null;
|
||||
@@ -665,6 +681,8 @@ Http::init()
|
||||
if ($file->isEmpty()) {
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
Span::add('storage.bucket.id', $bucketId);
|
||||
Span::add('storage.file.id', $fileId);
|
||||
// Do not update transformedAt if it's a console user
|
||||
if (! $user->isPrivileged($authorization->getRoles())) {
|
||||
$transformedAt = $file->getAttribute('transformedAt', '');
|
||||
@@ -677,16 +695,27 @@ Http::init()
|
||||
}
|
||||
}
|
||||
|
||||
$accessedAt = $cacheLog->getAttribute('accessedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), new Document([
|
||||
'accessedAt' => DateTime::now(),
|
||||
])));
|
||||
// Refresh the filesystem file's mtime so TTL-based expiry in cache->load() stays valid
|
||||
$cache->save($key, $data);
|
||||
}
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', sprintf('private, max-age=%d', $timestamp))
|
||||
->addHeader('X-Appwrite-Cache', 'hit')
|
||||
->setContentType($cacheLog->getAttribute('mimeType'));
|
||||
$storageCacheOperationsCounter->add(1, ['result' => 'hit']);
|
||||
if (! $isImageTransformation || ! $isDisabled) {
|
||||
Span::add('storage.cache.hit', true);
|
||||
$response->send($data);
|
||||
}
|
||||
} else {
|
||||
$storageCacheOperationsCounter->add(1, ['result' => 'miss']);
|
||||
Span::add('storage.cache.hit', false);
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
@@ -724,7 +753,12 @@ Http::shutdown()
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, Database $dbForProject) {
|
||||
$sessionLimit = $project->getAttribute('auths', [])['maxSessions'] ?? APP_LIMIT_USER_SESSIONS_DEFAULT;
|
||||
$sessionLimit = $project->getAttribute('auths', [])['maxSessions'] ?? 0;
|
||||
|
||||
if ($sessionLimit === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$session = $response->getPayload();
|
||||
$userId = $session['userId'] ?? '';
|
||||
if (empty($userId)) {
|
||||
@@ -758,7 +792,8 @@ Http::shutdown()
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForAudits')
|
||||
->inject('auditContext')
|
||||
->inject('publisherForAudits')
|
||||
->inject('usage')
|
||||
->inject('publisherForUsage')
|
||||
->inject('queueForDeletes')
|
||||
@@ -774,7 +809,8 @@ Http::shutdown()
|
||||
->inject('eventProcessor')
|
||||
->inject('bus')
|
||||
->inject('apiKey')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey) use ($parseLabel) {
|
||||
->inject('mode')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
|
||||
|
||||
$responsePayload = $response->getPayload();
|
||||
|
||||
@@ -869,16 +905,18 @@ Http::shutdown()
|
||||
if (! empty($pattern)) {
|
||||
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
|
||||
if (! empty($resource) && $resource !== $pattern) {
|
||||
$queueForAudits->setResource($resource);
|
||||
$auditContext->resource = $resource;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $user->isEmpty()) {
|
||||
$userClone = clone $user;
|
||||
// $user doesn't support `type` and can cause unintended effects.
|
||||
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
|
||||
$queueForAudits->setUser($userClone);
|
||||
} elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) {
|
||||
if (empty($user->getAttribute('type'))) {
|
||||
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
|
||||
}
|
||||
$auditContext->user = $userClone;
|
||||
} elseif ($auditContext->user === null || $auditContext->user->isEmpty()) {
|
||||
/**
|
||||
* User in the request is empty, and no user was set for auditing previously.
|
||||
* This indicates:
|
||||
@@ -896,24 +934,21 @@ Http::shutdown()
|
||||
'name' => 'Guest',
|
||||
]);
|
||||
|
||||
$queueForAudits->setUser($user);
|
||||
$auditContext->user = $user;
|
||||
}
|
||||
|
||||
if (! empty($queueForAudits->getResource()) && ! $queueForAudits->getUser()->isEmpty()) {
|
||||
$auditUser = $auditContext->user;
|
||||
if (! empty($auditContext->resource) && ! $auditUser->isEmpty()) {
|
||||
/**
|
||||
* audits.payload is switched to default true
|
||||
* in order to auto audit payload for all endpoints
|
||||
*/
|
||||
$pattern = $route->getLabel('audits.payload', true);
|
||||
if (! empty($pattern)) {
|
||||
$queueForAudits->setPayload($responsePayload);
|
||||
$auditContext->payload = $responsePayload;
|
||||
}
|
||||
|
||||
foreach ($queueForEvents->getParams() as $key => $value) {
|
||||
$queueForAudits->setParam($key, $value);
|
||||
}
|
||||
|
||||
$queueForAudits->trigger();
|
||||
$publisherForAudits->enqueue(AuditMessage::fromContext($auditContext));
|
||||
}
|
||||
|
||||
if (! empty($queueForDeletes->getType())) {
|
||||
@@ -937,7 +972,8 @@ Http::shutdown()
|
||||
if ($useCache) {
|
||||
$resource = $resourceType = null;
|
||||
$data = $response->getPayload();
|
||||
if (! empty($data['payload'])) {
|
||||
$statusCode = $response->getStatusCode();
|
||||
if (! empty($data['payload']) && $statusCode >= 200 && $statusCode < 300) {
|
||||
$pattern = $route->getLabel('cache.resource', null);
|
||||
if (! empty($pattern)) {
|
||||
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
|
||||
|
||||
+64
-69
@@ -1,14 +1,13 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
require_once __DIR__ . '/init.php';
|
||||
require_once __DIR__ . '/init/span.php';
|
||||
|
||||
$registerRequestResources = require __DIR__ . '/init/resources/request.php';
|
||||
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Swoole\Constant;
|
||||
use Swoole\Http\Request as SwooleRequest;
|
||||
use Swoole\Http\Response as SwooleResponse;
|
||||
use Swoole\Http\Server;
|
||||
use Swoole\Process;
|
||||
use Swoole\Table;
|
||||
use Swoole\Timer;
|
||||
@@ -27,11 +26,11 @@ use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Http\Adapter\Swoole\Server;
|
||||
use Utopia\Http\Files;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Logger\Log\User;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Span\Span;
|
||||
use Utopia\System\System;
|
||||
|
||||
@@ -48,18 +47,33 @@ $certifiedDomains = new Table(100_000);
|
||||
$certifiedDomains->column('value', Table::TYPE_INT, 1);
|
||||
$certifiedDomains->create();
|
||||
|
||||
Http::setResource('riskyDomains', fn () => $riskyDomains);
|
||||
Http::setResource('certifiedDomains', fn () => $certifiedDomains);
|
||||
|
||||
$http = new Server(
|
||||
host: "0.0.0.0",
|
||||
port: System::getEnv('PORT', 80),
|
||||
mode: SWOOLE_PROCESS,
|
||||
);
|
||||
global $container;
|
||||
$container->set('riskyDomains', fn () => $riskyDomains);
|
||||
$container->set('certifiedDomains', fn () => $certifiedDomains);
|
||||
$container->set('pools', function ($register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
|
||||
$payloadSize = 12 * (1024 * 1024); // 12MB - adding slight buffer for headers and other data that might be sent with the payload - update later with valid testing
|
||||
$totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
|
||||
|
||||
$swooleAdapter = new Server(
|
||||
host: "0.0.0.0",
|
||||
port: System::getEnv('PORT', 80),
|
||||
settings: [
|
||||
Constant::OPTION_WORKER_NUM => $totalWorkers,
|
||||
Constant::OPTION_DISPATCH_FUNC => dispatch(...),
|
||||
Constant::OPTION_DISPATCH_MODE => SWOOLE_DISPATCH_UIDMOD,
|
||||
Constant::OPTION_HTTP_COMPRESSION => false,
|
||||
Constant::OPTION_PACKAGE_MAX_LENGTH => $payloadSize,
|
||||
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
|
||||
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
|
||||
],
|
||||
container: $container,
|
||||
);
|
||||
|
||||
$http = $swooleAdapter->getServer();
|
||||
|
||||
/**
|
||||
* Assigns HTTP requests to worker threads by analyzing its payload/content.
|
||||
*
|
||||
@@ -68,16 +82,16 @@ $totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intva
|
||||
* riskier tasks to a dedicated worker subset. Prefers idle workers, with fallback to random selection if necessary.
|
||||
* doc: https://openswoole.com/docs/modules/swoole-server/configuration#dispatch_func
|
||||
*
|
||||
* @param Server $server Swoole server instance.
|
||||
* @param \Swoole\Http\Server $server Swoole server instance.
|
||||
* @param int $fd client ID
|
||||
* @param int $type the type of data and its current state
|
||||
* @param string|null $data Request content for categorization.
|
||||
* @global int $totalThreads Total number of workers.
|
||||
* @return int Chosen worker ID for the request.
|
||||
*/
|
||||
function dispatch(Server $server, int $fd, int $type, $data = null): int
|
||||
function dispatch(\Swoole\Http\Server $server, int $fd, int $type, $data = null): int
|
||||
{
|
||||
$resolveWorkerId = function (Server $server, $data = null) {
|
||||
$resolveWorkerId = function (\Swoole\Http\Server $server, $data = null) {
|
||||
global $totalWorkers, $riskyDomains;
|
||||
|
||||
// If data is not set we can send request to any worker
|
||||
@@ -160,18 +174,6 @@ function dispatch(Server $server, int $fd, int $type, $data = null): int
|
||||
return $workerId;
|
||||
}
|
||||
|
||||
|
||||
$http
|
||||
->set([
|
||||
Constant::OPTION_WORKER_NUM => $totalWorkers,
|
||||
Constant::OPTION_DISPATCH_FUNC => dispatch(...),
|
||||
Constant::OPTION_DISPATCH_MODE => SWOOLE_DISPATCH_UIDMOD,
|
||||
Constant::OPTION_HTTP_COMPRESSION => false,
|
||||
Constant::OPTION_PACKAGE_MAX_LENGTH => $payloadSize,
|
||||
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
|
||||
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
|
||||
]);
|
||||
|
||||
$http->on(Constant::EVENT_WORKER_START, function ($server, $workerId) {
|
||||
});
|
||||
|
||||
@@ -188,16 +190,14 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
|
||||
Console::success('Reload completed...');
|
||||
});
|
||||
|
||||
Http::setResource('bus', function ($register, $utopia) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $utopia->getResource($name));
|
||||
}, ['register', 'utopia']);
|
||||
$container->set('bus', function ($register) use ($swooleAdapter) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $swooleAdapter->getContainer()->get($name));
|
||||
}, ['register']);
|
||||
|
||||
include __DIR__ . '/controllers/general.php';
|
||||
|
||||
function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
|
||||
{
|
||||
$max = 15;
|
||||
$sleep = 2;
|
||||
$max = 15;
|
||||
$sleep = 2;
|
||||
$attempts = 0;
|
||||
@@ -288,13 +288,13 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c
|
||||
Span::current()?->finish();
|
||||
}
|
||||
|
||||
$http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $totalWorkers, $register) {
|
||||
$app = new Http('UTC');
|
||||
$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $swooleAdapter) {
|
||||
$app = new Http($swooleAdapter, 'UTC');
|
||||
|
||||
go(function () use ($register, $app) {
|
||||
$pools = $register->get('pools');
|
||||
/** @var Group $pools */
|
||||
Http::setResource('pools', fn () => $pools);
|
||||
/** @var \Utopia\Pools\Group $pools */
|
||||
$pools = $app->getResource('pools');
|
||||
|
||||
go(function () use ($app, $pools) {
|
||||
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
@@ -413,27 +413,19 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $tot
|
||||
$projectCollections = $collections['projects'];
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
$sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', ''));
|
||||
$sharedTablesV2 = \array_diff($sharedTables, $sharedTablesV1);
|
||||
|
||||
$documentsSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''));
|
||||
$documentsSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', ''));
|
||||
$documentsSharedTablesV2 = \array_diff($documentsSharedTables, $documentsSharedTablesV1);
|
||||
|
||||
$vectorSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''));
|
||||
$vectorSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', ''));
|
||||
$vectorSharedTablesV2 = \array_diff($vectorSharedTables, $vectorSharedTablesV1);
|
||||
|
||||
$cache = $app->getResource('cache');
|
||||
|
||||
// All shared tables V2 pools that need project metadata collections
|
||||
$sharedTablesV2All = \array_values(\array_unique(\array_filter([
|
||||
...$sharedTablesV2,
|
||||
...$documentsSharedTablesV2,
|
||||
...$vectorSharedTablesV2,
|
||||
// All shared tables pools that need project metadata collections
|
||||
$allSharedTables = \array_values(\array_unique(\array_filter([
|
||||
...$sharedTables,
|
||||
...$documentsSharedTables,
|
||||
...$vectorSharedTables,
|
||||
])));
|
||||
|
||||
foreach ($sharedTablesV2All as $hostname) {
|
||||
foreach ($allSharedTables as $hostname) {
|
||||
Span::init('database.setup');
|
||||
Span::add('database.hostname', $hostname);
|
||||
|
||||
@@ -510,14 +502,11 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $tot
|
||||
});
|
||||
});
|
||||
|
||||
$http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register, $files) {
|
||||
$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swooleAdapter, $registerRequestResources) {
|
||||
Span::init('http.request');
|
||||
|
||||
Http::setResource('swooleRequest', fn () => $swooleRequest);
|
||||
Http::setResource('swooleResponse', fn () => $swooleResponse);
|
||||
|
||||
$request = new Request($swooleRequest);
|
||||
$response = new Response($swooleResponse);
|
||||
$request = new Request($utopiaRequest->getSwooleRequest());
|
||||
$response = new Response($utopiaResponse->getSwooleResponse());
|
||||
|
||||
Span::add('http.method', $request->getMethod());
|
||||
|
||||
@@ -533,13 +522,19 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
|
||||
return;
|
||||
}
|
||||
|
||||
$app = new Http('UTC');
|
||||
$requestContainer = $swooleAdapter->getContainer();
|
||||
$requestContainer->set('container', fn () => $requestContainer);
|
||||
$requestContainer->set('request', fn () => $request);
|
||||
$requestContainer->set('response', fn () => $response);
|
||||
|
||||
$app = new Http($swooleAdapter, 'UTC');
|
||||
$requestContainer->set('utopia', fn () => $app);
|
||||
|
||||
$registerRequestResources($requestContainer);
|
||||
|
||||
$app->setCompression(System::getEnv('_APP_COMPRESSION_ENABLED', 'enabled') === 'enabled');
|
||||
$app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB
|
||||
|
||||
$pools = $register->get('pools');
|
||||
Http::setResource('pools', fn () => $pools);
|
||||
|
||||
try {
|
||||
$authorization = $app->getResource('authorization');
|
||||
|
||||
@@ -623,6 +618,7 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
|
||||
}
|
||||
}
|
||||
|
||||
$swooleResponse = $utopiaResponse->getSwooleResponse();
|
||||
$swooleResponse->setStatusCode(500);
|
||||
|
||||
$output = ((Http::isDevelopment())) ? [
|
||||
@@ -646,16 +642,15 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
|
||||
});
|
||||
|
||||
// Fetch domains every `DOMAIN_SYNC_TIMER` seconds and update in the memory
|
||||
$http->on(Constant::EVENT_TASK, function () use ($register) {
|
||||
$http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
|
||||
$lastSyncUpdate = null;
|
||||
$pools = $register->get('pools');
|
||||
Http::setResource('pools', fn () => $pools);
|
||||
$app = new Http('UTC');
|
||||
|
||||
$app = new Http($swooleAdapter, 'UTC');
|
||||
|
||||
/** @var Utopia\Database\Database $dbForPlatform */
|
||||
$dbForPlatform = $app->getResource('dbForPlatform');
|
||||
|
||||
/** @var Table $riskyDomains */
|
||||
/** @var \Swoole\Table $riskyDomains */
|
||||
$riskyDomains = $app->getResource('riskyDomains');
|
||||
|
||||
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $app) {
|
||||
@@ -725,4 +720,4 @@ $http->on(Constant::EVENT_TASK, function () use ($register) {
|
||||
});
|
||||
});
|
||||
|
||||
$http->start();
|
||||
$swooleAdapter->start();
|
||||
|
||||
@@ -12,7 +12,7 @@ Config::load('runtimes-v2', __DIR__ . '/../config/runtimes-v2.php', $configAdapt
|
||||
Config::load('template-runtimes', __DIR__ . '/../config/template-runtimes.php', $configAdapter);
|
||||
Config::load('events', __DIR__ . '/../config/events.php', $configAdapter);
|
||||
Config::load('auth', __DIR__ . '/../config/auth.php', $configAdapter);
|
||||
Config::load('apis', __DIR__ . '/../config/apis.php', $configAdapter); // List of APIs
|
||||
Config::load('protocols', __DIR__ . '/../config/protocols.php', $configAdapter);
|
||||
Config::load('errors', __DIR__ . '/../config/errors.php', $configAdapter);
|
||||
Config::load('oAuthProviders', __DIR__ . '/../config/oAuthProviders.php', $configAdapter);
|
||||
Config::load('sdks', __DIR__ . '/../config/sdks.php', $configAdapter);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Platform\Modules\Compute\Specification;
|
||||
use Utopia\System\System;
|
||||
|
||||
const APP_NAME = 'Appwrite';
|
||||
const APP_DOMAIN = 'appwrite.io';
|
||||
@@ -24,9 +25,6 @@ const APP_MODE_ADMIN = 'admin';
|
||||
const APP_PAGING_LIMIT = 12;
|
||||
const APP_LIMIT_COUNT = 5000;
|
||||
const APP_LIMIT_USERS = 10_000;
|
||||
const APP_LIMIT_USER_PASSWORD_HISTORY = 20;
|
||||
const APP_LIMIT_USER_SESSIONS_MAX = 100;
|
||||
const APP_LIMIT_USER_SESSIONS_DEFAULT = 10;
|
||||
const APP_LIMIT_ANTIVIRUS = 20_000_000; //20MB
|
||||
const APP_LIMIT_ENCRYPTION = 20_000_000; //20MB
|
||||
const APP_LIMIT_COMPRESSION = 20_000_000; //20MB
|
||||
@@ -46,8 +44,8 @@ const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_BUSTER = 4321;
|
||||
const APP_VERSION_STABLE = '1.9.0';
|
||||
const APP_CACHE_BUSTER = 4323;
|
||||
const APP_VERSION_STABLE = '1.9.2';
|
||||
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
|
||||
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
|
||||
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
|
||||
@@ -156,9 +154,12 @@ const SESSION_PROVIDER_SERVER = 'server';
|
||||
/**
|
||||
* Activity associated with user or the app.
|
||||
*/
|
||||
const ACTIVITY_TYPE_APP = 'app';
|
||||
const ACTIVITY_TYPE_USER = 'user';
|
||||
const ACTIVITY_TYPE_ADMIN = 'admin';
|
||||
const ACTIVITY_TYPE_GUEST = 'guest';
|
||||
const ACTIVITY_TYPE_KEY_PROJECT = 'keyProject';
|
||||
const ACTIVITY_TYPE_KEY_ACCOUNT = 'keyAccount';
|
||||
const ACTIVITY_TYPE_KEY_ORGANIZATION = 'keyOrganization';
|
||||
|
||||
/**
|
||||
* MFA
|
||||
@@ -187,7 +188,7 @@ const BUILD_TYPE_RETRY = 'retry';
|
||||
|
||||
// Deletion Types
|
||||
|
||||
const ENABLE_EXECUTIONS_LIMIT_ON_ROUTE = false;
|
||||
\define('ENABLE_EXECUTIONS_LIMIT_ON_ROUTE', System::getEnv('_APP_EXECUTIONS_LIMIT_ON_ROUTE', 'disabled') === 'enabled');
|
||||
|
||||
const DELETE_TYPE_DATABASES = 'databases';
|
||||
const DELETE_TYPE_DOCUMENT = 'document';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\OpenSSL\OpenSSL;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
@@ -123,11 +124,17 @@ Database::addFilter(
|
||||
return;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
return $database->getAuthorization()->skip(fn () => $database
|
||||
$platforms = $database->getAuthorization()->skip(fn () => $database
|
||||
->find('platforms', [
|
||||
Query::equal('projectInternalId', [$document->getSequence()]),
|
||||
Query::limit(APP_LIMIT_SUBQUERY),
|
||||
]));
|
||||
|
||||
foreach ($platforms as $platform) {
|
||||
$platform->setAttribute('type', Platform::mapDeprecatedType($platform->getAttribute('type')));
|
||||
}
|
||||
|
||||
return $platforms;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
+16
-7
@@ -106,13 +106,20 @@ use Appwrite\Utopia\Response\Model\Mock;
|
||||
use Appwrite\Utopia\Response\Model\MockNumber;
|
||||
use Appwrite\Utopia\Response\Model\None;
|
||||
use Appwrite\Utopia\Response\Model\Phone;
|
||||
use Appwrite\Utopia\Response\Model\Platform;
|
||||
use Appwrite\Utopia\Response\Model\PlatformAndroid;
|
||||
use Appwrite\Utopia\Response\Model\PlatformApple;
|
||||
use Appwrite\Utopia\Response\Model\PlatformLinux;
|
||||
use Appwrite\Utopia\Response\Model\PlatformList;
|
||||
use Appwrite\Utopia\Response\Model\PlatformWeb;
|
||||
use Appwrite\Utopia\Response\Model\PlatformWindows;
|
||||
use Appwrite\Utopia\Response\Model\Preferences;
|
||||
use Appwrite\Utopia\Response\Model\Project;
|
||||
use Appwrite\Utopia\Response\Model\Provider;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepository;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryFramework;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryFrameworkList;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntime;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntimeList;
|
||||
use Appwrite\Utopia\Response\Model\ResourceToken;
|
||||
use Appwrite\Utopia\Response\Model\Row;
|
||||
use Appwrite\Utopia\Response\Model\Rule;
|
||||
@@ -130,7 +137,6 @@ use Appwrite\Utopia\Response\Model\TemplateFramework;
|
||||
use Appwrite\Utopia\Response\Model\TemplateFunction;
|
||||
use Appwrite\Utopia\Response\Model\TemplateRuntime;
|
||||
use Appwrite\Utopia\Response\Model\TemplateSite;
|
||||
use Appwrite\Utopia\Response\Model\TemplateSMS;
|
||||
use Appwrite\Utopia\Response\Model\TemplateVariable;
|
||||
use Appwrite\Utopia\Response\Model\Token;
|
||||
use Appwrite\Utopia\Response\Model\Topic;
|
||||
@@ -185,8 +191,8 @@ Response::setModel(new BaseList('Site Templates List', Response::MODEL_TEMPLATE_
|
||||
Response::setModel(new BaseList('Functions List', Response::MODEL_FUNCTION_LIST, 'functions', Response::MODEL_FUNCTION));
|
||||
Response::setModel(new BaseList('Function Templates List', Response::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', Response::MODEL_TEMPLATE_FUNCTION));
|
||||
Response::setModel(new BaseList('Installations List', Response::MODEL_INSTALLATION_LIST, 'installations', Response::MODEL_INSTALLATION));
|
||||
Response::setModel(new BaseList('Framework Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, 'frameworkProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK));
|
||||
Response::setModel(new BaseList('Runtime Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, 'runtimeProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME));
|
||||
Response::setModel(new ProviderRepositoryFrameworkList());
|
||||
Response::setModel(new ProviderRepositoryRuntimeList());
|
||||
Response::setModel(new BaseList('Branches List', Response::MODEL_BRANCH_LIST, 'branches', Response::MODEL_BRANCH));
|
||||
Response::setModel(new BaseList('Frameworks List', Response::MODEL_FRAMEWORK_LIST, 'frameworks', Response::MODEL_FRAMEWORK));
|
||||
Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, 'runtimes', Response::MODEL_RUNTIME));
|
||||
@@ -197,7 +203,6 @@ Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, '
|
||||
Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, true));
|
||||
Response::setModel(new BaseList('Dev Keys List', Response::MODEL_DEV_KEY_LIST, 'devKeys', Response::MODEL_DEV_KEY, true, false));
|
||||
Response::setModel(new BaseList('Auth Providers List', Response::MODEL_AUTH_PROVIDER_LIST, 'platforms', Response::MODEL_AUTH_PROVIDER, true, false));
|
||||
Response::setModel(new BaseList('Platforms List', Response::MODEL_PLATFORM_LIST, 'platforms', Response::MODEL_PLATFORM, true, false));
|
||||
Response::setModel(new BaseList('Countries List', Response::MODEL_COUNTRY_LIST, 'countries', Response::MODEL_COUNTRY));
|
||||
Response::setModel(new BaseList('Continents List', Response::MODEL_CONTINENT_LIST, 'continents', Response::MODEL_CONTINENT));
|
||||
Response::setModel(new BaseList('Languages List', Response::MODEL_LANGUAGE_LIST, 'languages', Response::MODEL_LANGUAGE));
|
||||
@@ -333,7 +338,12 @@ Response::setModel(new Key());
|
||||
Response::setModel(new DevKey());
|
||||
Response::setModel(new MockNumber());
|
||||
Response::setModel(new AuthProvider());
|
||||
Response::setModel(new Platform());
|
||||
Response::setModel(new PlatformWeb());
|
||||
Response::setModel(new PlatformApple());
|
||||
Response::setModel(new PlatformAndroid());
|
||||
Response::setModel(new PlatformWindows());
|
||||
Response::setModel(new PlatformLinux());
|
||||
Response::setModel(new PlatformList());
|
||||
Response::setModel(new Variable());
|
||||
Response::setModel(new Country());
|
||||
Response::setModel(new Continent());
|
||||
@@ -364,7 +374,6 @@ Response::setModel(new Headers());
|
||||
Response::setModel(new Specification());
|
||||
Response::setModel(new Rule());
|
||||
Response::setModel(new Schedule());
|
||||
Response::setModel(new TemplateSMS());
|
||||
Response::setModel(new TemplateEmail());
|
||||
Response::setModel(new ConsoleVariables());
|
||||
Response::setModel(new MFAChallenge());
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
<?php
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Utopia\Auth\Hashes\Sha;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Auth\Store;
|
||||
use Utopia\Database\DateTime as DatabaseDateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DI\Container;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\URL;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
/**
|
||||
* Register the minimal per-connection resources required by realtime.
|
||||
*/
|
||||
return function (Container $container): void {
|
||||
$getProjectId = static function (Request $request): string {
|
||||
$projectId = $request->getHeader('x-appwrite-project', '');
|
||||
|
||||
if (!empty($projectId)) {
|
||||
return $projectId;
|
||||
}
|
||||
|
||||
$projectId = $request->getParam('project', '');
|
||||
|
||||
return \is_string($projectId) ? $projectId : '';
|
||||
};
|
||||
|
||||
$getMode = static function (Request $request, Document $project) use ($getProjectId): string {
|
||||
$mode = $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT));
|
||||
$projectId = $getProjectId($request);
|
||||
|
||||
if (!empty($projectId) && $project->getId() !== $projectId) {
|
||||
$mode = APP_MODE_ADMIN;
|
||||
}
|
||||
|
||||
return $mode;
|
||||
};
|
||||
|
||||
$getDbForPlatform = static function (Authorization $authorization) {
|
||||
$database = getConsoleDB();
|
||||
$database->setAuthorization($authorization);
|
||||
|
||||
return $database;
|
||||
};
|
||||
|
||||
$getDbForProject = static function (Document $project, Authorization $authorization) use ($getDbForPlatform) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $getDbForPlatform($authorization);
|
||||
}
|
||||
|
||||
$database = getProjectDB($project);
|
||||
$database->setAuthorization($authorization);
|
||||
|
||||
return $database;
|
||||
};
|
||||
|
||||
$findRule = static function (Request $request, Document $project, Authorization $authorization) use ($getDbForPlatform): Document {
|
||||
$domain = \parse_url($request->getOrigin(), PHP_URL_HOST);
|
||||
|
||||
if (empty($domain)) {
|
||||
$domain = \parse_url($request->getReferer(), PHP_URL_HOST);
|
||||
}
|
||||
|
||||
if (empty($domain)) {
|
||||
return new Document();
|
||||
}
|
||||
|
||||
$dbForPlatform = $getDbForPlatform($authorization);
|
||||
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
|
||||
|
||||
$rule = $authorization->skip(function () use ($dbForPlatform, $domain, $isMd5) {
|
||||
if ($isMd5) {
|
||||
return $dbForPlatform->getDocument('rules', md5($domain));
|
||||
}
|
||||
|
||||
return $dbForPlatform->findOne('rules', [
|
||||
Query::equal('domain', [$domain]),
|
||||
]);
|
||||
});
|
||||
|
||||
$permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence();
|
||||
|
||||
if (!$permitsCurrentProject && !$rule->isEmpty() && !empty($rule->getAttribute('projectId', ''))) {
|
||||
$trustedProjects = [];
|
||||
foreach (\explode(',', System::getEnv('_APP_CONSOLE_TRUSTED_PROJECTS', '')) as $trustedProject) {
|
||||
if (empty($trustedProject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$trustedProjects[] = $trustedProject;
|
||||
}
|
||||
|
||||
if (\in_array($rule->getAttribute('projectId', ''), $trustedProjects, true)) {
|
||||
$permitsCurrentProject = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$permitsCurrentProject) {
|
||||
return new Document();
|
||||
}
|
||||
|
||||
return $rule;
|
||||
};
|
||||
|
||||
$findDevKey = static function (Request $request, Document $project, array $servers, Authorization $authorization) use ($getDbForPlatform): Document {
|
||||
$devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', ''));
|
||||
$key = $project->find('secret', $devKey, 'devKeys');
|
||||
|
||||
if (!$key) {
|
||||
return new Document([]);
|
||||
}
|
||||
|
||||
$expire = $key->getAttribute('expire');
|
||||
if (!empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
|
||||
return new Document([]);
|
||||
}
|
||||
|
||||
$dbForPlatform = $getDbForPlatform($authorization);
|
||||
$accessedAt = $key->getAttribute('accessedAt', 0);
|
||||
|
||||
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
|
||||
$key->setAttribute('accessedAt', DatabaseDateTime::now());
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([
|
||||
'accessedAt' => $key->getAttribute('accessedAt'),
|
||||
])));
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
|
||||
$sdkValidator = new WhiteList($servers, true);
|
||||
$sdk = \strtolower($request->getHeader('x-sdk-name', 'UNKNOWN'));
|
||||
|
||||
if ($sdk !== 'unknown' && $sdkValidator->isValid($sdk)) {
|
||||
$sdks = $key->getAttribute('sdks', []);
|
||||
|
||||
if (!\in_array($sdk, $sdks, true)) {
|
||||
$sdks[] = $sdk;
|
||||
$key->setAttribute('sdks', $sdks);
|
||||
$key->setAttribute('accessedAt', DatabaseDateTime::now());
|
||||
|
||||
$key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([
|
||||
'sdks' => $key->getAttribute('sdks'),
|
||||
'accessedAt' => $key->getAttribute('accessedAt'),
|
||||
])));
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
}
|
||||
|
||||
return $key;
|
||||
};
|
||||
|
||||
$container->set('authorization', function () {
|
||||
return new Authorization();
|
||||
}, []);
|
||||
|
||||
$container->set('project', function (Request $request, Document $console, Authorization $authorization) use ($getProjectId, $getDbForPlatform) {
|
||||
$projectId = $getProjectId($request);
|
||||
|
||||
if (empty($projectId) || $projectId === 'console') {
|
||||
return $console;
|
||||
}
|
||||
|
||||
$dbForPlatform = $getDbForPlatform($authorization);
|
||||
|
||||
return $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
|
||||
}, ['request', 'console', 'authorization']);
|
||||
|
||||
$container->set('originValidator', function (array $platform, Request $request, Document $project, array $servers, Authorization $authorization) use ($findDevKey, $findRule) {
|
||||
$devKey = $findDevKey($request, $project, $servers, $authorization);
|
||||
|
||||
if (!$devKey->isEmpty()) {
|
||||
return new URL();
|
||||
}
|
||||
|
||||
$allowedHostnames = [...($platform['hostnames'] ?? [])];
|
||||
if (!$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$allowedHostnames = [...$allowedHostnames, ...Platform::getHostnames($project->getAttribute('platforms', []))];
|
||||
}
|
||||
|
||||
$rule = $findRule($request, $project, $authorization);
|
||||
if (!$rule->isEmpty() && !empty($rule->getAttribute('domain', ''))) {
|
||||
$allowedHostnames[] = $rule->getAttribute('domain', '');
|
||||
}
|
||||
|
||||
$originHostname = \parse_url($request->getOrigin(), PHP_URL_HOST);
|
||||
$refererHostname = \parse_url($request->getReferer(), PHP_URL_HOST);
|
||||
$hostname = $originHostname ?: $refererHostname;
|
||||
|
||||
if ($request->getMethod() === 'OPTIONS' && !empty($hostname)) {
|
||||
$allowedHostnames[] = $hostname;
|
||||
}
|
||||
|
||||
$allowedSchemes = [...($platform['schemas'] ?? [])];
|
||||
if (!$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$allowedSchemes[] = 'exp';
|
||||
$allowedSchemes[] = 'appwrite-callback-' . $project->getId();
|
||||
$allowedSchemes = [...$allowedSchemes, ...Platform::getSchemes($project->getAttribute('platforms', []))];
|
||||
}
|
||||
|
||||
return new Origin(\array_unique($allowedHostnames), \array_unique($allowedSchemes));
|
||||
}, ['platform', 'request', 'project', 'servers', 'authorization']);
|
||||
|
||||
$container->set('user', function (Request $request, Document $project, Document $console, Authorization $authorization) use ($getMode, $getDbForPlatform, $getDbForProject) {
|
||||
$mode = $getMode($request, $project);
|
||||
$store = new Store();
|
||||
$proofForToken = new Token();
|
||||
$proofForToken->setHash(new Sha());
|
||||
|
||||
$authorization->setDefaultStatus(true);
|
||||
|
||||
$dbForPlatform = $getDbForPlatform($authorization);
|
||||
$dbForProject = $getDbForProject($project, $authorization);
|
||||
|
||||
$store->setKey('a_session_' . $project->getId());
|
||||
if ($mode === APP_MODE_ADMIN) {
|
||||
$store->setKey('a_session_' . $console->getId());
|
||||
}
|
||||
|
||||
$store->decode(
|
||||
$request->getCookie(
|
||||
$store->getKey(),
|
||||
$request->getCookie($store->getKey() . '_legacy', '')
|
||||
)
|
||||
);
|
||||
|
||||
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
|
||||
$sessionHeader = $request->getHeader('x-appwrite-session', '');
|
||||
|
||||
if (!empty($sessionHeader)) {
|
||||
$store->decode($sessionHeader);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
|
||||
$fallback = \json_decode($request->getHeader('x-fallback-cookies', ''), true);
|
||||
$store->decode((\is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : '');
|
||||
}
|
||||
|
||||
$user = null;
|
||||
if ($mode === APP_MODE_ADMIN) {
|
||||
/** @var User $user */
|
||||
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
|
||||
} else {
|
||||
if ($project->isEmpty()) {
|
||||
$user = new User([]);
|
||||
} elseif (!empty($store->getProperty('id', ''))) {
|
||||
if ($project->getId() === 'console') {
|
||||
/** @var User $user */
|
||||
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
|
||||
} else {
|
||||
/** @var User $user */
|
||||
$user = $dbForProject->getDocument('users', $store->getProperty('id', ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!$user
|
||||
|| $user->isEmpty()
|
||||
|| !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken)
|
||||
) {
|
||||
$user = new User([]);
|
||||
}
|
||||
|
||||
$authJWT = $request->getHeader('x-appwrite-jwt', '');
|
||||
if (!empty($authJWT) && !$project->isEmpty()) {
|
||||
if (!$user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_JWT_AND_COOKIE_SET);
|
||||
}
|
||||
|
||||
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
|
||||
|
||||
try {
|
||||
$payload = $jwt->decode($authJWT);
|
||||
} catch (JWTException $error) {
|
||||
throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage());
|
||||
}
|
||||
|
||||
$jwtUserId = $payload['userId'] ?? '';
|
||||
if (!empty($jwtUserId)) {
|
||||
if ($mode === APP_MODE_ADMIN) {
|
||||
$user = $dbForPlatform->getDocument('users', $jwtUserId);
|
||||
} else {
|
||||
$user = $dbForProject->getDocument('users', $jwtUserId);
|
||||
}
|
||||
}
|
||||
|
||||
$jwtSessionId = $payload['sessionId'] ?? '';
|
||||
if (!empty($jwtSessionId) && empty($user->find('$id', $jwtSessionId, 'sessions'))) {
|
||||
$user = new User([]);
|
||||
}
|
||||
}
|
||||
|
||||
$accountKey = $request->getHeader('x-appwrite-key', '');
|
||||
$accountKeyUserId = $request->getHeader('x-appwrite-user', '');
|
||||
|
||||
if (!empty($accountKeyUserId) && !empty($accountKey)) {
|
||||
if (!$user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
|
||||
}
|
||||
|
||||
$accountKeyUser = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId));
|
||||
if (!$accountKeyUser->isEmpty()) {
|
||||
$key = $accountKeyUser->find(
|
||||
key: 'secret',
|
||||
find: $accountKey,
|
||||
subject: 'keys'
|
||||
);
|
||||
|
||||
if (!empty($key)) {
|
||||
$expire = $key->getAttribute('expire');
|
||||
if (!empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
|
||||
throw new Exception(Exception::ACCOUNT_KEY_EXPIRED);
|
||||
}
|
||||
|
||||
$user = $accountKeyUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$impersonateUserId = $request->getHeader('x-appwrite-impersonate-user-id', '');
|
||||
$impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', '');
|
||||
$impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', '');
|
||||
|
||||
if (!$user->isEmpty() && $user->getAttribute('impersonator', false)) {
|
||||
$userDb = ($mode === APP_MODE_ADMIN || $project->getId() === 'console') ? $dbForPlatform : $dbForProject;
|
||||
$targetUser = null;
|
||||
|
||||
if (!empty($impersonateUserId)) {
|
||||
$targetUser = $authorization->skip(fn () => $userDb->getDocument('users', $impersonateUserId));
|
||||
} elseif (!empty($impersonateEmail)) {
|
||||
$targetUser = $authorization->skip(fn () => $userDb->findOne('users', [
|
||||
Query::equal('email', [\strtolower($impersonateEmail)]),
|
||||
]));
|
||||
} elseif (!empty($impersonatePhone)) {
|
||||
$targetUser = $authorization->skip(fn () => $userDb->findOne('users', [
|
||||
Query::equal('phone', [$impersonatePhone]),
|
||||
]));
|
||||
}
|
||||
|
||||
if ($targetUser !== null && !$targetUser->isEmpty()) {
|
||||
$impersonator = clone $user;
|
||||
$user = clone $targetUser;
|
||||
$user->setAttribute('impersonatorUserId', $impersonator->getId());
|
||||
$user->setAttribute('impersonatorUserInternalId', $impersonator->getSequence());
|
||||
$user->setAttribute('impersonatorUserName', $impersonator->getAttribute('name', ''));
|
||||
$user->setAttribute('impersonatorUserEmail', $impersonator->getAttribute('email', ''));
|
||||
$user->setAttribute('impersonatorAccessedAt', $impersonator->getAttribute('accessedAt', 0));
|
||||
}
|
||||
}
|
||||
|
||||
$dbForPlatform->setMetadata('user', $user->getId());
|
||||
$dbForProject->setMetadata('user', $user->getId());
|
||||
|
||||
return $user;
|
||||
}, ['request', 'project', 'console', 'authorization']);
|
||||
};
|
||||
+29
-56
@@ -6,7 +6,6 @@ use Appwrite\Hooks\Hooks;
|
||||
use Appwrite\PubSub\Adapter\Redis as PubSub;
|
||||
use Appwrite\URL\URL as AppwriteURL;
|
||||
use MaxMind\Db\Reader;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Swoole\Database\PDOProxy;
|
||||
use Utopia\Cache\Adapter\Redis as RedisCache;
|
||||
use Utopia\Config\Config;
|
||||
@@ -25,6 +24,7 @@ use Utopia\Logger\Adapter\LogOwl;
|
||||
use Utopia\Logger\Adapter\Raygun;
|
||||
use Utopia\Logger\Adapter\Sentry;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Messaging\Adapter\Email\SMTP;
|
||||
use Utopia\Mongo\Client as MongoClient;
|
||||
use Utopia\Pools\Adapter\Stack as StackPool;
|
||||
use Utopia\Pools\Adapter\Swoole as SwoolePool;
|
||||
@@ -56,7 +56,7 @@ $register->set('logger', function () {
|
||||
}
|
||||
|
||||
try {
|
||||
$loggingProvider = new DSN($providerConfig ?? '');
|
||||
$loggingProvider = new DSN($providerConfig);
|
||||
|
||||
$providerName = $loggingProvider->getScheme();
|
||||
$providerConfig = match ($providerName) {
|
||||
@@ -71,12 +71,12 @@ $register->set('logger', function () {
|
||||
|
||||
$providerConfig = match ($providerName) {
|
||||
'sentry' => [ 'key' => $configChunks[0], 'projectId' => $configChunks[1] ?? '', 'host' => '',],
|
||||
'logowl' => ['ticket' => $configChunks[0] ?? '', 'host' => ''],
|
||||
'logowl' => ['ticket' => $configChunks[0], 'host' => ''],
|
||||
default => ['key' => $providerConfig],
|
||||
};
|
||||
}
|
||||
|
||||
if (empty($providerName) || empty($providerConfig)) {
|
||||
if (empty($providerName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ $register->set('realtimeLogger', function () {
|
||||
default => ['key' => $loggingProvider->getHost()],
|
||||
};
|
||||
|
||||
if (empty($providerName) || empty($providerConfig)) {
|
||||
if (empty($providerName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -242,29 +242,18 @@ $register->set('pools', function () {
|
||||
],
|
||||
];
|
||||
|
||||
$maxConnections = System::getEnv('_APP_CONNECTIONS_MAX', 151);
|
||||
$instanceConnections = $maxConnections / System::getEnv('_APP_POOL_CLIENTS', 14);
|
||||
$maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151);
|
||||
$instanceConnections = $maxConnections / (int) System::getEnv('_APP_POOL_CLIENTS', 14);
|
||||
|
||||
$multiprocessing = System::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled';
|
||||
|
||||
if ($multiprocessing) {
|
||||
$workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
|
||||
} else {
|
||||
$workerCount = 1;
|
||||
}
|
||||
|
||||
if ($workerCount > $instanceConnections) {
|
||||
throw new \Exception('Pool size is too small. Increase the number of allowed database connections or decrease the number of workers.', 500);
|
||||
}
|
||||
|
||||
$poolSize = (int)($instanceConnections / $workerCount);
|
||||
$workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
|
||||
$poolSize = max(1, (int)($instanceConnections / $workerCount));
|
||||
|
||||
foreach ($connections as $key => $connection) {
|
||||
$type = $connection['type'] ?? '';
|
||||
$multiple = $connection['multiple'] ?? false;
|
||||
$schemes = $connection['schemes'] ?? [];
|
||||
$type = $connection['type'];
|
||||
$multiple = $connection['multiple'];
|
||||
$schemes = $connection['schemes'];
|
||||
$config = [];
|
||||
$dsns = explode(',', $connection['dsns'] ?? '');
|
||||
$dsns = explode(',', $connection['dsns']);
|
||||
foreach ($dsns as &$dsn) {
|
||||
$dsn = explode('=', $dsn);
|
||||
$name = ($multiple) ? $key . '_' . $dsn[0] : $key;
|
||||
@@ -308,7 +297,7 @@ $register->set('pools', function () {
|
||||
]);
|
||||
});
|
||||
},
|
||||
'mongodb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase, $dsn) {
|
||||
'mongodb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) {
|
||||
try {
|
||||
$mongo = new MongoClient($dsnDatabase, $dsnHost, (int)$dsnPort, $dsnUser, $dsnPass, false);
|
||||
@$mongo->connect();
|
||||
@@ -329,7 +318,7 @@ $register->set('pools', function () {
|
||||
));
|
||||
});
|
||||
},
|
||||
'redis' => function () use ($dsnHost, $dsnPort, $dsnPass) {
|
||||
default => function () use ($dsnHost, $dsnPort, $dsnPass) {
|
||||
$redis = new \Redis();
|
||||
@$redis->pconnect($dsnHost, (int)$dsnPort);
|
||||
if ($dsnPass) {
|
||||
@@ -339,7 +328,6 @@ $register->set('pools', function () {
|
||||
|
||||
return $redis;
|
||||
},
|
||||
default => throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Invalid scheme'),
|
||||
};
|
||||
|
||||
$poolAdapter = System::getEnv('_APP_POOL_ADAPTER', default: 'stack') === 'swoole' ? new SwoolePool() : new StackPool();
|
||||
@@ -433,35 +421,20 @@ $register->set('db', function () {
|
||||
});
|
||||
|
||||
$register->set('smtp', function () {
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
$mail->isSMTP();
|
||||
|
||||
$username = System::getEnv('_APP_SMTP_USERNAME');
|
||||
$password = System::getEnv('_APP_SMTP_PASSWORD');
|
||||
|
||||
$mail->XMailer = 'Appwrite Mailer';
|
||||
$mail->Host = System::getEnv('_APP_SMTP_HOST', 'smtp');
|
||||
$mail->Port = System::getEnv('_APP_SMTP_PORT', 25);
|
||||
$mail->SMTPAuth = !empty($username) && !empty($password);
|
||||
$mail->Username = $username;
|
||||
$mail->Password = $password;
|
||||
$mail->SMTPSecure = System::getEnv('_APP_SMTP_SECURE', '');
|
||||
$mail->SMTPAutoTLS = false;
|
||||
$mail->SMTPKeepAlive = true;
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->Timeout = 10; /* Connection timeout */
|
||||
$mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */
|
||||
|
||||
$from = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
|
||||
$email = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
|
||||
|
||||
$mail->setFrom($email, $from);
|
||||
$mail->addReplyTo($email, $from);
|
||||
|
||||
$mail->isHTML(true);
|
||||
|
||||
return $mail;
|
||||
$username = System::getEnv('_APP_SMTP_USERNAME', '');
|
||||
$password = System::getEnv('_APP_SMTP_PASSWORD', '');
|
||||
return new SMTP(
|
||||
host: System::getEnv('_APP_SMTP_HOST', 'smtp'),
|
||||
port: (int) System::getEnv('_APP_SMTP_PORT', 25),
|
||||
username: $username,
|
||||
password: $password,
|
||||
smtpSecure: System::getEnv('_APP_SMTP_SECURE', ''),
|
||||
smtpAutoTLS: false,
|
||||
xMailer: 'Appwrite Mailer',
|
||||
timeout: 10,
|
||||
keepAlive: true,
|
||||
timelimit: 30,
|
||||
);
|
||||
});
|
||||
$register->set('geodb', function () {
|
||||
return new Reader(__DIR__ . '/../assets/dbip/dbip-country-lite-2025-12.mmdb');
|
||||
|
||||
+69
-1395
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,436 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Utopia\Audit\Adapter\Database as AdapterDatabase;
|
||||
use Utopia\Audit\Audit as UtopiaAudit;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Adapter\Pool as DatabasePool;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DI\Container;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Registry\Registry;
|
||||
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter as Telemetry;
|
||||
|
||||
/**
|
||||
* Register per-job resources on the given container.
|
||||
* These resources depend on the queue message or keep mutable state and
|
||||
* must be fresh for each worker job.
|
||||
*/
|
||||
return function (Container $container): void {
|
||||
$container->set('log', fn () => new Log(), []);
|
||||
|
||||
$container->set('usage', fn () => new Context(), []);
|
||||
|
||||
$container->set('authorization', function () {
|
||||
$authorization = new Authorization();
|
||||
$authorization->disable();
|
||||
|
||||
return $authorization;
|
||||
}, []);
|
||||
|
||||
$container->set('dbForPlatform', function (Cache $cache, Group $pools, Authorization $authorization) {
|
||||
$adapter = new DatabasePool($pools->get('console'));
|
||||
$dbForPlatform = new Database($adapter, $cache);
|
||||
|
||||
$dbForPlatform
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setNamespace('_console')
|
||||
->setDocumentType('users', User::class);
|
||||
|
||||
return $dbForPlatform;
|
||||
}, ['cache', 'pools', 'authorization']);
|
||||
|
||||
$container->set('project', function ($message, Database $dbForPlatform) {
|
||||
$payload = $message->getPayload() ?? [];
|
||||
$project = new Document($payload['project'] ?? []);
|
||||
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $project;
|
||||
}
|
||||
|
||||
return $dbForPlatform->getDocument('projects', $project->getId());
|
||||
}, ['message', 'dbForPlatform']);
|
||||
|
||||
$container->set('dbForProject', function (Cache $cache, Group $pools, Document $project, Database $dbForPlatform, Authorization $authorization) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
}
|
||||
|
||||
try {
|
||||
$dsn = new DSN($project->getAttribute('database'));
|
||||
} catch (\InvalidArgumentException) {
|
||||
// TODO: Temporary until all projects are using shared tables
|
||||
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
|
||||
}
|
||||
|
||||
$adapter = new DatabasePool($pools->get($dsn->getHost()));
|
||||
$database = new Database($adapter, $cache);
|
||||
$database->setDocumentType('users', User::class);
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
|
||||
return $database;
|
||||
}, ['cache', 'pools', 'project', 'dbForPlatform', 'authorization']);
|
||||
|
||||
$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) {
|
||||
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
|
||||
|
||||
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
}
|
||||
|
||||
try {
|
||||
$dsn = new DSN($project->getAttribute('database'));
|
||||
} catch (\InvalidArgumentException) {
|
||||
// TODO: Temporary until all projects are using shared tables
|
||||
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
|
||||
}
|
||||
|
||||
if (isset($databases[$dsn->getHost()])) {
|
||||
$database = $databases[$dsn->getHost()];
|
||||
$database->setAuthorization($authorization);
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
return $database;
|
||||
}
|
||||
|
||||
$adapter = new DatabasePool($pools->get($dsn->getHost()));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
$databases[$dsn->getHost()] = $database;
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
|
||||
|
||||
$container->set('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) {
|
||||
return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database {
|
||||
$projectDocument ??= $project;
|
||||
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
|
||||
$databaseType = $database->getAttribute('type', '');
|
||||
|
||||
// Backwards-compatibility: older or seeded legacy databases may not have a DSN stored
|
||||
// in the "database" attribute. In that case, fall back to the project's database DSN.
|
||||
if ($databaseDSN === '') {
|
||||
$databaseDSN = $projectDocument->getAttribute('database', '');
|
||||
}
|
||||
|
||||
try {
|
||||
$databaseDSN = new DSN($databaseDSN);
|
||||
} catch (\InvalidArgumentException) {
|
||||
$databaseDSN = new DSN('mysql://' . $databaseDSN);
|
||||
}
|
||||
|
||||
try {
|
||||
$dsn = new DSN($projectDocument->getAttribute('database'));
|
||||
} catch (\InvalidArgumentException) {
|
||||
// Temporary fallback until all projects use shared tables
|
||||
$dsn = new DSN('mysql://' . $projectDocument->getAttribute('database'));
|
||||
}
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$databaseHost = $databaseDSN->getHost();
|
||||
$pool = $pools->get($databaseHost);
|
||||
|
||||
$adapter = new DatabasePool($pool);
|
||||
$database = new Database($adapter, $cache);
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization);
|
||||
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
|
||||
|
||||
$sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')));
|
||||
|
||||
// For separate pools (documentsdb/vectorsdb), check their own shared tables config.
|
||||
// If not configured, use dedicated mode to avoid cross-engine tenant type mismatches.
|
||||
if ($databaseHost !== $dsn->getHost()) {
|
||||
$dbTypeSharedTables = match ($databaseType) {
|
||||
DOCUMENTSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''))),
|
||||
VECTORSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''))),
|
||||
default => [],
|
||||
};
|
||||
|
||||
if (\in_array($databaseHost, $dbTypeSharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($projectDocument->getSequence())
|
||||
->setNamespace($databaseDSN->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $projectDocument->getSequence());
|
||||
}
|
||||
} elseif (\in_array($dsn->getHost(), $sharedTables, true)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($projectDocument->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $projectDocument->getSequence());
|
||||
}
|
||||
|
||||
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
return $database;
|
||||
};
|
||||
}, ['cache', 'register', 'project', 'authorization']);
|
||||
|
||||
$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
$database = null;
|
||||
|
||||
return function (?Document $project = null) use ($pools, $cache, $authorization, &$database) {
|
||||
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant($project->getSequence());
|
||||
|
||||
return $database;
|
||||
}
|
||||
|
||||
$adapter = new DatabasePool($pools->get('logs'));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setSharedTables(true)
|
||||
->setNamespace('logsV1')
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
|
||||
|
||||
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant($project->getSequence());
|
||||
}
|
||||
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
$container->set('abuseRetention', function () {
|
||||
return \time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day
|
||||
}, []);
|
||||
|
||||
$container->set('auditRetention', function (Document $project) {
|
||||
if ($project->getId() === 'console') {
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
|
||||
}
|
||||
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
|
||||
}, ['project']);
|
||||
|
||||
$container->set('executionRetention', function () {
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
|
||||
}, []);
|
||||
|
||||
$container->set('queueForDatabase', function (Publisher $publisher) {
|
||||
return new EventDatabase($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForMessaging', function (Publisher $publisher) {
|
||||
return new Messaging($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForMails', function (Publisher $publisher) {
|
||||
return new Mail($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForBuilds', function (Publisher $publisher) {
|
||||
return new Build($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForDeletes', function (Publisher $publisher) {
|
||||
return new Delete($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForEvents', function (Publisher $publisher) {
|
||||
return new Event($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForWebhooks', function (Publisher $publisher) {
|
||||
return new Webhook($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForFunctions', function (Publisher $publisher) {
|
||||
return new Func($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForRealtime', function () {
|
||||
return new Realtime();
|
||||
}, []);
|
||||
|
||||
$container->set('deviceForSites', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
$container->set('deviceForMigrations', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
$container->set('deviceForFunctions', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
$container->set('deviceForFiles', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
$container->set('deviceForBuilds', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
$container->set('deviceForCache', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
$container->set('logError', function (Registry $register, Document $project) {
|
||||
return function (Throwable $error, string $namespace, string $action, ?array $extras = null) use ($register, $project) {
|
||||
$logger = $register->get('logger');
|
||||
|
||||
if ($logger) {
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
$log = new Log();
|
||||
$log->setNamespace($namespace);
|
||||
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('verboseType', \get_class($error));
|
||||
$log->addTag('projectId', $project->getId());
|
||||
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
|
||||
if ($error->getPrevious() !== null) {
|
||||
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
|
||||
$log->addExtra('previousMessage', $error->getPrevious()->getMessage());
|
||||
}
|
||||
$log->addExtra('previousFile', $error->getPrevious()->getFile());
|
||||
$log->addExtra('previousLine', $error->getPrevious()->getLine());
|
||||
}
|
||||
|
||||
foreach (($extras ?? []) as $key => $value) {
|
||||
$log->addExtra($key, $value);
|
||||
}
|
||||
|
||||
$log->setAction($action);
|
||||
|
||||
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
try {
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Error log pushed with status code: ' . $responseCode);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Error pushing log: ' . $th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
Console::warning("Failed: {$error->getMessage()}");
|
||||
Console::warning($error->getTraceAsString());
|
||||
|
||||
if ($error->getPrevious() !== null) {
|
||||
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
|
||||
Console::warning("Previous Failed: {$error->getPrevious()->getMessage()}");
|
||||
}
|
||||
Console::warning("Previous File: {$error->getPrevious()->getFile()} Line: {$error->getPrevious()->getLine()}");
|
||||
}
|
||||
};
|
||||
}, ['register', 'project']);
|
||||
|
||||
$container->set('getAudit', function (Database $dbForPlatform, callable $getProjectDB) {
|
||||
return function (Document $project) use ($dbForPlatform, $getProjectDB) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
$adapter = new AdapterDatabase($dbForPlatform);
|
||||
|
||||
return new UtopiaAudit($adapter);
|
||||
}
|
||||
|
||||
$dbForProject = $getProjectDB($project);
|
||||
$adapter = new AdapterDatabase($dbForProject);
|
||||
|
||||
return new UtopiaAudit($adapter);
|
||||
};
|
||||
}, ['dbForPlatform', 'getProjectDB']);
|
||||
|
||||
$container->set('executionsRetentionCount', function (Document $project, array $plan) {
|
||||
if ($project->getId() === 'console' || empty($plan)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) ($plan['executionsRetentionCount'] ?? 100);
|
||||
}, ['project', 'plan']);
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Bus\Listeners\Log;
|
||||
use Appwrite\Bus\Listeners\Mails;
|
||||
use Appwrite\Bus\Listeners\Usage;
|
||||
|
||||
return [
|
||||
new Log(),
|
||||
new Mails(),
|
||||
new Usage(),
|
||||
];
|
||||
|
||||
+312
-47
@@ -33,8 +33,8 @@ use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DI\Container;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Registry\Registry;
|
||||
@@ -43,11 +43,13 @@ use Utopia\Telemetry\Adapter\None as NoTelemetry;
|
||||
use Utopia\WebSocket\Adapter;
|
||||
use Utopia\WebSocket\Server;
|
||||
|
||||
/**
|
||||
* @var Registry $register
|
||||
*/
|
||||
require_once __DIR__ . '/init.php';
|
||||
|
||||
/** @var Registry $register */
|
||||
$register = $GLOBALS['register'] ?? throw new \RuntimeException('Registry not initialized');
|
||||
|
||||
$registerConnectionResources ??= require __DIR__ . '/init/realtime/connection.php';
|
||||
|
||||
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
|
||||
|
||||
// Log uncaught exceptions in one line instead of relying on Swoole's full backtrace dump
|
||||
@@ -237,10 +239,14 @@ if (!function_exists('getTelemetry')) {
|
||||
if (!function_exists('triggerStats')) {
|
||||
function triggerStats(array $event, string $projectId): void
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
global $container;
|
||||
$container->set('pools', function ($register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
|
||||
$realtime = getRealtime();
|
||||
|
||||
/**
|
||||
@@ -256,7 +262,9 @@ $stats->create();
|
||||
|
||||
$containerId = uniqid();
|
||||
$statsDocument = null;
|
||||
$workerNumber = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
|
||||
|
||||
$workerNumber = intval(System::getEnv('_APP_WORKERS_NUM', 0))
|
||||
?: intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
|
||||
|
||||
$adapter = new Adapter\Swoole(port: System::getEnv('PORT', 80));
|
||||
$adapter
|
||||
@@ -320,14 +328,14 @@ if (!function_exists('logError')) {
|
||||
|
||||
$server->error(logError(...));
|
||||
|
||||
$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument) {
|
||||
$server->onStart(function () use ($stats, $containerId, &$statsDocument) {
|
||||
sleep(5); // wait for the initial database schema to be ready
|
||||
Console::success('Server started successfully');
|
||||
|
||||
/**
|
||||
* Create document for this worker to share stats across Containers.
|
||||
*/
|
||||
go(function () use ($register, $containerId, &$statsDocument) {
|
||||
go(function () use ($containerId, &$statsDocument) {
|
||||
$attempts = 0;
|
||||
$database = getConsoleDB();
|
||||
|
||||
@@ -357,7 +365,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
|
||||
*/
|
||||
// TODO: Remove this if check once it doesn't cause issues for cloud
|
||||
if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') {
|
||||
Timer::tick(5000, function () use ($register, $stats, &$statsDocument) {
|
||||
Timer::tick(5000, function () use ($stats, &$statsDocument) {
|
||||
$payload = [];
|
||||
foreach ($stats as $projectId => $value) {
|
||||
$payload[$projectId] = $stats->get($projectId, 'connectionsTotal');
|
||||
@@ -388,15 +396,32 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
Console::success('Worker ' . $workerId . ' started successfully');
|
||||
|
||||
$telemetry = getTelemetry($workerId);
|
||||
$realtimeDelayBuckets = [100, 250, 500, 750, 1000, 1500, 2000, 3000, 5000, 7500, 10000, 15000, 30000];
|
||||
$workerTelemetryAttributes = ['workerId' => (string) $workerId];
|
||||
$register->set('telemetry', fn () => $telemetry);
|
||||
$register->set('telemetry.workerAttributes', fn () => $workerTelemetryAttributes);
|
||||
$register->set('telemetry.workerCounter', fn () => $telemetry->createUpDownCounter('realtime.server.active_workers'));
|
||||
$register->set('telemetry.workerClientCounter', fn () => $telemetry->createUpDownCounter('realtime.server.worker_clients'));
|
||||
$register->set('telemetry.workerSubscriptionCounter', fn () => $telemetry->createUpDownCounter('realtime.server.worker_subscriptions'));
|
||||
$register->set('telemetry.connectionCounter', fn () => $telemetry->createUpDownCounter('realtime.server.open_connections'));
|
||||
$register->set('telemetry.connectionCreatedCounter', fn () => $telemetry->createCounter('realtime.server.connection.created'));
|
||||
$register->set('telemetry.messageSentCounter', fn () => $telemetry->createCounter('realtime.server.message.sent'));
|
||||
$register->set('telemetry.deliveryDelayHistogram', fn () => $telemetry->createHistogram(
|
||||
name: 'realtime.server.delivery_delay',
|
||||
unit: 'ms',
|
||||
advisory: ['ExplicitBucketBoundaries' => $realtimeDelayBuckets],
|
||||
));
|
||||
$register->set('telemetry.arrivalDelayHistogram', fn () => $telemetry->createHistogram(
|
||||
name: 'realtime.server.arrival_delay',
|
||||
unit: 'ms',
|
||||
advisory: ['ExplicitBucketBoundaries' => $realtimeDelayBuckets],
|
||||
));
|
||||
$register->get('telemetry.workerCounter')->add(1);
|
||||
|
||||
$attempts = 0;
|
||||
$start = time();
|
||||
|
||||
Timer::tick(5000, function () use ($server, $register, $realtime, $stats) {
|
||||
Timer::tick(5000, function () use ($server, $realtime, $stats) {
|
||||
/**
|
||||
* Sending current connections to project channels on the console project every 5 seconds.
|
||||
*/
|
||||
@@ -442,7 +467,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
]
|
||||
];
|
||||
|
||||
$server->send($realtime->getSubscribers($event), json_encode([
|
||||
$server->send(array_keys($realtime->getSubscribers($event)), json_encode([
|
||||
'type' => 'event',
|
||||
'data' => $event['data']
|
||||
]));
|
||||
@@ -508,12 +533,28 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
$pubsub->subscribe(['realtime'], function (mixed $redis, string $channel, string $payload) use ($server, $workerId, $stats, $register, $realtime) {
|
||||
$event = json_decode($payload, true);
|
||||
|
||||
$eventTimestamp = $event['data']['timestamp'] ?? null;
|
||||
if (\is_string($eventTimestamp)) {
|
||||
try {
|
||||
$eventDate = new \DateTimeImmutable($eventTimestamp, new \DateTimeZone('UTC'));
|
||||
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
|
||||
$eventTimestampMs = (float) $eventDate->format('U.u') * 1000;
|
||||
$nowTimestampMs = (float) $now->format('U.u') * 1000;
|
||||
$arrivalDelayMs = (int) \max(0, $nowTimestampMs - $eventTimestampMs);
|
||||
|
||||
$register->get('telemetry.arrivalDelayHistogram')->record($arrivalDelayMs);
|
||||
} catch (\Throwable) {
|
||||
// Ignore invalid timestamp payloads.
|
||||
}
|
||||
}
|
||||
|
||||
if ($event['permissionsChanged'] && isset($event['userId'])) {
|
||||
$projectId = $event['project'];
|
||||
$userId = $event['userId'];
|
||||
|
||||
if ($realtime->hasSubscriber($projectId, 'user:' . $userId)) {
|
||||
$connection = array_key_first(reset($realtime->subscriptions[$projectId]['user:' . $userId]));
|
||||
$subscriptionsBefore = \count($realtime->getSubscriptionMetadata($connection));
|
||||
$consoleDatabase = getConsoleDB();
|
||||
$project = $consoleDatabase->getAuthorization()->skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
|
||||
$database = getProjectDB($project);
|
||||
@@ -544,12 +585,18 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
if ($authorization !== null) {
|
||||
$realtime->connections[$connection]['authorization'] = $authorization;
|
||||
}
|
||||
|
||||
$subscriptionsAfter = \count($realtime->getSubscriptionMetadata($connection));
|
||||
$subscriptionDelta = $subscriptionsAfter - $subscriptionsBefore;
|
||||
if ($subscriptionDelta !== 0) {
|
||||
$register->get('telemetry.workerSubscriptionCounter')->add($subscriptionDelta, $register->get('telemetry.workerAttributes'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
if (Http::isDevelopment() && !empty($receivers)) {
|
||||
if (System::getEnv('_APP_ENV', 'production') === 'development' && !empty($receivers)) {
|
||||
Console::log("[Debug][Worker {$workerId}] Receivers: " . count($receivers));
|
||||
Console::log("[Debug][Worker {$workerId}] Connection IDs: " . json_encode(array_keys($receivers)));
|
||||
Console::log("[Debug][Worker {$workerId}] Matched: " . json_encode(array_values($receivers)));
|
||||
@@ -586,6 +633,20 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
if ($total > 0) {
|
||||
$register->get('telemetry.messageSentCounter')->add($total);
|
||||
$stats->incr($event['project'], 'messages', $total);
|
||||
$updatedAt = $event['data']['payload']['$updatedAt'] ?? null;
|
||||
if (\is_string($updatedAt)) {
|
||||
try {
|
||||
$updatedAtDate = new \DateTimeImmutable($updatedAt, new \DateTimeZone('UTC'));
|
||||
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
|
||||
$updatedAtTimestampMs = (float) $updatedAtDate->format('U.u') * 1000;
|
||||
$nowTimestampMs = (float) $now->format('U.u') * 1000;
|
||||
$delayMs = (int) \max(0, $nowTimestampMs - $updatedAtTimestampMs);
|
||||
|
||||
$register->get('telemetry.deliveryDelayHistogram')->record($delayMs);
|
||||
} catch (\Throwable) {
|
||||
// Ignore invalid timestamp payloads.
|
||||
}
|
||||
}
|
||||
|
||||
$projectId = $event['project'] ?? null;
|
||||
|
||||
@@ -615,16 +676,27 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
Console::error('Failed to restart pub/sub...');
|
||||
});
|
||||
|
||||
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime) {
|
||||
$app = new Http('UTC');
|
||||
$server->onWorkerStop(function (int $workerId) use ($register) {
|
||||
Console::warning('Worker ' . $workerId . ' stopping');
|
||||
|
||||
try {
|
||||
$register->get('telemetry.workerCounter')->add(-1);
|
||||
} catch (\Throwable $th) {
|
||||
Console::error('Realtime onWorkerStop telemetry error: ' . $th->getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $registerConnectionResources) {
|
||||
global $container;
|
||||
$request = new Request($request);
|
||||
$response = new Response(new SwooleResponse());
|
||||
|
||||
Console::info("Connection open (user: {$connection})");
|
||||
|
||||
Http::setResource('pools', fn () => $register->get('pools'));
|
||||
Http::setResource('request', fn () => $request);
|
||||
Http::setResource('response', fn () => $response);
|
||||
$connectionContainer = new Container($container);
|
||||
$connectionContainer->set('request', fn () => $request);
|
||||
|
||||
$registerConnectionResources($connectionContainer);
|
||||
|
||||
$project = null;
|
||||
$logUser = null;
|
||||
@@ -632,8 +704,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
|
||||
try {
|
||||
/** @var Document $project */
|
||||
$project = $app->getResource('project');
|
||||
$authorization = $app->getResource('authorization');
|
||||
$project = $connectionContainer->get('project');
|
||||
$authorization = $connectionContainer->get('authorization');
|
||||
|
||||
/*
|
||||
* Project Check
|
||||
@@ -642,13 +714,15 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing or unknown project ID');
|
||||
}
|
||||
|
||||
$timelimit = $app->getResource('timelimit');
|
||||
$user = $app->getResource('user'); /** @var User $user */
|
||||
$timelimit = $connectionContainer->get('timelimit');
|
||||
$user = $connectionContainer->get('user'); /** @var User $user */
|
||||
$logUser = $user;
|
||||
|
||||
$apis = $project->getAttribute('apis', []);
|
||||
// Websocket is what to check, but realtime is checked too for backwards compatibility
|
||||
$websocketEnabled = $apis['websocket'] ?? $apis['realtime'] ?? true;
|
||||
if (
|
||||
array_key_exists('realtime', $project->getAttribute('apis', []))
|
||||
&& !$project->getAttribute('apis', [])['realtime']
|
||||
!$websocketEnabled
|
||||
&& !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
@@ -688,7 +762,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
* Skip this check for non-web platforms which are not required to send an origin header.
|
||||
*/
|
||||
$origin = $request->getOrigin();
|
||||
$originValidator = $app->getResource('originValidator');
|
||||
$originValidator = $connectionContainer->get('originValidator');
|
||||
|
||||
if (!empty($origin) && !$originValidator->isValid($origin) && $project->getId() !== 'console') {
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
|
||||
@@ -698,11 +772,44 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
|
||||
$channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId());
|
||||
|
||||
$updateStats = static function (string $projectId, ?string $teamId, string $payloadJson) use ($register, $stats): void {
|
||||
$register->get('telemetry.connectionCounter')->add(1);
|
||||
$register->get('telemetry.workerClientCounter')->add(1, $register->get('telemetry.workerAttributes'));
|
||||
$register->get('telemetry.connectionCreatedCounter')->add(1);
|
||||
|
||||
$stats->set($projectId, [
|
||||
'projectId' => $projectId,
|
||||
'teamId' => $teamId
|
||||
]);
|
||||
$stats->incr($projectId, 'connections');
|
||||
$stats->incr($projectId, 'connectionsTotal');
|
||||
|
||||
triggerStats([
|
||||
METRIC_REALTIME_CONNECTIONS => 1,
|
||||
METRIC_REALTIME_OUTBOUND => \strlen($payloadJson),
|
||||
], $projectId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Channels Check
|
||||
*/
|
||||
if (empty($channels)) {
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing channels');
|
||||
// in case of message based 'subscribe' channels will be empty at first and only projectId and roles will be available
|
||||
$sanitizedUser = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT);
|
||||
$connectedPayloadJson = json_encode([
|
||||
'type' => 'connected',
|
||||
'data' => [
|
||||
'channels' => [],
|
||||
'subscriptions' => [],
|
||||
'user' => $sanitizedUser
|
||||
]
|
||||
]);
|
||||
|
||||
$realtime->subscribe($project->getId(), $connection, '', $roles, [], [], $user->getId());
|
||||
$realtime->connections[$connection]['authorization'] = $authorization;
|
||||
$server->send([$connection], $connectedPayloadJson);
|
||||
$updateStats($project->getId(), $project->getAttribute('teamId'), $connectedPayloadJson);
|
||||
return;
|
||||
}
|
||||
|
||||
$names = array_keys($channels);
|
||||
@@ -726,11 +833,15 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
$subscriptionId,
|
||||
$roles,
|
||||
$subscription['channels'],
|
||||
$subscription['queries']
|
||||
$subscription['queries'],
|
||||
$user->getId()
|
||||
);
|
||||
|
||||
$mapping[$index] = $subscriptionId;
|
||||
}
|
||||
if (!empty($subscriptions)) {
|
||||
$register->get('telemetry.workerSubscriptionCounter')->add(\count($subscriptions), $register->get('telemetry.workerAttributes'));
|
||||
}
|
||||
|
||||
$realtime->connections[$connection]['authorization'] = $authorization;
|
||||
|
||||
@@ -746,20 +857,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
]);
|
||||
|
||||
$server->send([$connection], $connectedPayloadJson);
|
||||
|
||||
$register->get('telemetry.connectionCounter')->add(1);
|
||||
$register->get('telemetry.connectionCreatedCounter')->add(1);
|
||||
|
||||
$stats->set($project->getId(), [
|
||||
'projectId' => $project->getId(),
|
||||
'teamId' => $project->getAttribute('teamId')
|
||||
]);
|
||||
$stats->incr($project->getId(), 'connections');
|
||||
$stats->incr($project->getId(), 'connectionsTotal');
|
||||
|
||||
$connectedOutboundBytes = \strlen($connectedPayloadJson);
|
||||
|
||||
triggerStats([METRIC_REALTIME_CONNECTIONS => 1, METRIC_REALTIME_OUTBOUND => $connectedOutboundBytes], $project->getId());
|
||||
$updateStats($project->getId(), $project->getAttribute('teamId'), $connectedPayloadJson);
|
||||
|
||||
|
||||
} catch (Throwable $th) {
|
||||
@@ -775,7 +873,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
|
||||
// sanitize 0 && 5xx errors
|
||||
$realtimeViolation = $th instanceof AppwriteException && $th->getType() === AppwriteException::REALTIME_POLICY_VIOLATION;
|
||||
if (($code === 0 || $code >= 500) && !$realtimeViolation && !Http::isDevelopment()) {
|
||||
if (($code === 0 || $code >= 500) && !$realtimeViolation && System::getEnv('_APP_ENV', 'production') !== 'development') {
|
||||
$message = 'Error: Server Error';
|
||||
}
|
||||
|
||||
@@ -790,7 +888,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
$server->send([$connection], json_encode($response));
|
||||
$server->close($connection, $code);
|
||||
|
||||
if (Http::isDevelopment()) {
|
||||
if (System::getEnv('_APP_ENV', 'production') === 'development') {
|
||||
Console::error('[Error] Connection Error');
|
||||
Console::error('[Error] Code: ' . $response['data']['code']);
|
||||
Console::error('[Error] Message: ' . $response['data']['message']);
|
||||
@@ -798,10 +896,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
}
|
||||
});
|
||||
|
||||
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) {
|
||||
$server->onMessage(function (int $connection, string $message) use ($server, $realtime, $containerId, $register) {
|
||||
$project = null;
|
||||
$authorization = null;
|
||||
|
||||
try {
|
||||
$rawSize = \strlen($message);
|
||||
$response = new Response(new SwooleResponse());
|
||||
@@ -810,7 +907,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
// Get authorization from connection (stored during onOpen)
|
||||
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
|
||||
if ($authorization === null) {
|
||||
$authorization = new Authorization('');
|
||||
$authorization = new Authorization();
|
||||
}
|
||||
|
||||
$database = getConsoleDB();
|
||||
@@ -913,6 +1010,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
|
||||
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
|
||||
|
||||
$subscriptionsBefore = \count($realtime->getSubscriptionMetadata($connection));
|
||||
$meta = $realtime->getSubscriptionMetadata($connection);
|
||||
|
||||
$realtime->unsubscribe($connection);
|
||||
@@ -927,7 +1025,8 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
$subscriptionId,
|
||||
$roles,
|
||||
$subscription['channels'] ?? [],
|
||||
$queries
|
||||
$queries,
|
||||
$user->getId()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -936,6 +1035,12 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
$realtime->connections[$connection]['authorization'] = $authorization;
|
||||
}
|
||||
|
||||
$subscriptionsAfter = \count($realtime->getSubscriptionMetadata($connection));
|
||||
$subscriptionDelta = $subscriptionsAfter - $subscriptionsBefore;
|
||||
if ($subscriptionDelta !== 0) {
|
||||
$register->get('telemetry.workerSubscriptionCounter')->add($subscriptionDelta, $register->get('telemetry.workerAttributes'));
|
||||
}
|
||||
|
||||
$user = $response->output($user, Response::MODEL_ACCOUNT);
|
||||
|
||||
$authResponsePayloadJson = json_encode([
|
||||
@@ -961,6 +1066,161 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
|
||||
break;
|
||||
|
||||
case 'subscribe':
|
||||
/**
|
||||
* Message based upsertion of a subscription
|
||||
* If subscriptionId is given then it will match subId of the connection and update the subscription with channels and queries
|
||||
* If non-existing subid is given or not given a new subid will be generated
|
||||
* Similar to what we have now -> two subscribe() block with same channels and queries still two different subscriptions
|
||||
*
|
||||
* structure of the payload -> array of maps
|
||||
* 'data' : [subscriptionId:"" , channels:[] , queries:[]]
|
||||
*/
|
||||
if (!is_array($message['data']) || !array_is_list($message['data'])) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
|
||||
}
|
||||
|
||||
$roles = $realtime->connections[$connection]['roles'] ?? [Role::guests()->toString()];
|
||||
$userId = $realtime->connections[$connection]['userId'] ?? '';
|
||||
|
||||
// bulk validation + parsing before subscribing
|
||||
$parsedPayloads = [];
|
||||
$subscriptionsBefore = \count($realtime->getSubscriptionMetadata($connection));
|
||||
foreach ($message['data'] as $payload) {
|
||||
if (!\is_array($payload)) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Each subscribe payload must be an object.');
|
||||
}
|
||||
if (!array_key_exists('channels', $payload)) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'channels is not present in payload.');
|
||||
}
|
||||
if (!is_array($payload['channels']) || !array_is_list($payload['channels'])) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'channels is not a valid array.');
|
||||
}
|
||||
// registering the queries if not present and check in the same payload later on
|
||||
if (!array_key_exists('queries', $payload)) {
|
||||
$payload['queries'] = [];
|
||||
}
|
||||
if (!is_array($payload['queries']) || !array_is_list($payload['queries'])) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'queries is not a valid array.');
|
||||
}
|
||||
|
||||
$subscriptionId = \array_key_exists('subscriptionId', $payload)
|
||||
? $payload['subscriptionId']
|
||||
: ID::unique();
|
||||
|
||||
try {
|
||||
$convertedQueries = Realtime::convertQueries($payload['queries']);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Invalid query: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$parsedPayloads[] = [
|
||||
'subscriptionId' => $subscriptionId,
|
||||
'channels' => $payload['channels'],
|
||||
'queries' => $convertedQueries,
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($parsedPayloads as $parsedPayload) {
|
||||
$subscriptionId = $parsedPayload['subscriptionId'];
|
||||
$channels = \array_keys(Realtime::convertChannels($parsedPayload['channels'], $userId));
|
||||
$queries = $parsedPayload['queries'];
|
||||
$realtime->subscribe($projectId, $connection, $subscriptionId, $roles, $channels, $queries);
|
||||
}
|
||||
$subscriptionsAfter = \count($realtime->getSubscriptionMetadata($connection));
|
||||
$subscriptionDelta = $subscriptionsAfter - $subscriptionsBefore;
|
||||
if ($subscriptionDelta !== 0) {
|
||||
$register->get('telemetry.workerSubscriptionCounter')->add($subscriptionDelta, $register->get('telemetry.workerAttributes'));
|
||||
}
|
||||
|
||||
$responsePayload = json_encode([
|
||||
'type' => 'response',
|
||||
'data' => [
|
||||
'to' => 'subscribe',
|
||||
'success' => true,
|
||||
'subscriptions' => \array_map(function (array $parsedPayload) {
|
||||
return [
|
||||
'subscriptionId' => $parsedPayload['subscriptionId'],
|
||||
'channels' => $parsedPayload['channels'],
|
||||
'queries' => \array_map(fn ($q) => $q->toString(), $parsedPayload['queries']),
|
||||
];
|
||||
}, $parsedPayloads),
|
||||
]
|
||||
]);
|
||||
|
||||
$server->send([$connection], $responsePayload);
|
||||
|
||||
if ($project !== null && !$project->isEmpty()) {
|
||||
$subscribeOutboundBytes = \strlen($responsePayload);
|
||||
|
||||
if ($subscribeOutboundBytes > 0) {
|
||||
triggerStats([
|
||||
METRIC_REALTIME_OUTBOUND => $subscribeOutboundBytes,
|
||||
], $project->getId());
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'unsubscribe':
|
||||
if (!\is_array($message['data']) || !\array_is_list($message['data'])) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
|
||||
}
|
||||
|
||||
$subscriptionsBefore = \count($realtime->getSubscriptionMetadata($connection));
|
||||
|
||||
// Validate every payload before executing any removal so an invalid entry
|
||||
// later in the batch does not leave earlier entries half-applied on the server.
|
||||
$validatedIds = [];
|
||||
foreach ($message['data'] as $payload) {
|
||||
if (
|
||||
!\is_array($payload)
|
||||
|| !\array_key_exists('subscriptionId', $payload)
|
||||
|| !\is_string($payload['subscriptionId'])
|
||||
|| $payload['subscriptionId'] === ''
|
||||
) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Each unsubscribe payload must include a non-empty subscriptionId.');
|
||||
}
|
||||
$validatedIds[] = $payload['subscriptionId'];
|
||||
}
|
||||
|
||||
$unsubscribeResults = [];
|
||||
foreach ($validatedIds as $subscriptionId) {
|
||||
$wasRemoved = $realtime->unsubscribeSubscription($connection, $subscriptionId);
|
||||
$unsubscribeResults[] = [
|
||||
'subscriptionId' => $subscriptionId,
|
||||
'removed' => $wasRemoved,
|
||||
];
|
||||
}
|
||||
$subscriptionsAfter = \count($realtime->getSubscriptionMetadata($connection));
|
||||
$subscriptionDelta = $subscriptionsAfter - $subscriptionsBefore;
|
||||
if ($subscriptionDelta !== 0) {
|
||||
$register->get('telemetry.workerSubscriptionCounter')->add($subscriptionDelta, $register->get('telemetry.workerAttributes'));
|
||||
}
|
||||
|
||||
$unsubscribeResponsePayload = json_encode([
|
||||
'type' => 'response',
|
||||
'data' => [
|
||||
'to' => 'unsubscribe',
|
||||
'success' => true,
|
||||
'subscriptions' => $unsubscribeResults,
|
||||
],
|
||||
]);
|
||||
|
||||
$server->send([$connection], $unsubscribeResponsePayload);
|
||||
|
||||
if ($project !== null && !$project->isEmpty()) {
|
||||
$unsubscribeOutboundBytes = \strlen($unsubscribeResponsePayload);
|
||||
|
||||
if ($unsubscribeOutboundBytes > 0) {
|
||||
triggerStats([
|
||||
METRIC_REALTIME_OUTBOUND => $unsubscribeOutboundBytes,
|
||||
], $project->getId());
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message type is not valid.');
|
||||
}
|
||||
@@ -974,7 +1234,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
$message = $th->getMessage();
|
||||
|
||||
// sanitize 0 && 5xx errors
|
||||
if (($code === 0 || $code >= 500) && !Http::isDevelopment()) {
|
||||
if (($code === 0 || $code >= 500) && System::getEnv('_APP_ENV', 'production') !== 'development') {
|
||||
$message = 'Error: Server Error';
|
||||
}
|
||||
|
||||
@@ -999,6 +1259,11 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register) {
|
||||
if (array_key_exists($connection, $realtime->connections)) {
|
||||
$stats->decr($realtime->connections[$connection]['projectId'], 'connectionsTotal');
|
||||
$register->get('telemetry.connectionCounter')->add(-1);
|
||||
$register->get('telemetry.workerClientCounter')->add(-1, $register->get('telemetry.workerAttributes'));
|
||||
$subscriptionsBeforeClose = \count($realtime->getSubscriptionMetadata($connection));
|
||||
if ($subscriptionsBeforeClose > 0) {
|
||||
$register->get('telemetry.workerSubscriptionCounter')->add(-$subscriptionsBeforeClose, $register->get('telemetry.workerAttributes'));
|
||||
}
|
||||
|
||||
$projectId = $realtime->connections[$connection]['projectId'];
|
||||
|
||||
|
||||
@@ -120,7 +120,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_SMTP_HOST
|
||||
- _APP_SMTP_PORT
|
||||
- _APP_SMTP_SECURE
|
||||
@@ -256,7 +255,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_USAGE_STATS
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
@@ -287,7 +285,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-webhooks:
|
||||
@@ -315,7 +312,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -356,7 +352,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_STORAGE_DEVICE
|
||||
- _APP_STORAGE_S3_ACCESS_KEY
|
||||
- _APP_STORAGE_S3_SECRET
|
||||
@@ -416,7 +411,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-builds:
|
||||
@@ -453,7 +447,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_VCS_GITHUB_APP_NAME
|
||||
- _APP_VCS_GITHUB_PRIVATE_KEY
|
||||
@@ -529,7 +522,35 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-executions:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
entrypoint: worker-executions
|
||||
<<: *x-logging
|
||||
container_name: appwrite-worker-executions
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
<?= $dbService ?>:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_ENV
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-functions:
|
||||
@@ -563,7 +584,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_SITES_TIMEOUT
|
||||
- _APP_COMPUTE_BUILD_TIMEOUT
|
||||
@@ -601,7 +621,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -644,7 +663,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_SMS_FROM
|
||||
- _APP_SMS_PROVIDER
|
||||
@@ -705,7 +723,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
|
||||
@@ -744,7 +761,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_MAINTENANCE_INTERVAL
|
||||
- _APP_MAINTENANCE_RETENTION_EXECUTION
|
||||
- _APP_MAINTENANCE_RETENTION_CACHE
|
||||
@@ -777,7 +793,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -810,7 +825,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -842,7 +856,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -878,7 +891,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
|
||||
appwrite-task-scheduler-executions:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
@@ -907,7 +919,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
|
||||
appwrite-task-scheduler-messages:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
@@ -936,7 +947,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
|
||||
<?php if ($enableAssistant): ?>
|
||||
appwrite-assistant:
|
||||
@@ -964,7 +974,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
<<: *x-logging
|
||||
restart: unless-stopped
|
||||
stop_signal: SIGINT
|
||||
image: openruntimes/executor:0.7.22
|
||||
image: openruntimes/executor:0.11.4
|
||||
networks:
|
||||
- appwrite
|
||||
- runtimes
|
||||
@@ -1039,13 +1049,12 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
image: mongo:8.2.5
|
||||
container_name: appwrite-mongodb
|
||||
<<: *x-logging
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- appwrite-mongodb:/data/db
|
||||
- appwrite-mongodb-keyfile:/data/keyfile
|
||||
ports:
|
||||
- "27017:27017"
|
||||
environment:
|
||||
- MONGO_INITDB_ROOT_USERNAME=root
|
||||
- MONGO_INITDB_ROOT_PASSWORD=${_APP_DB_ROOT_PASS}
|
||||
@@ -1176,7 +1185,6 @@ volumes:
|
||||
<?php elseif ($dbService === 'mongodb'): ?>
|
||||
appwrite-mongodb:
|
||||
appwrite-mongodb-keyfile:
|
||||
appwrite-mongodb-config:
|
||||
<?php endif; ?>
|
||||
appwrite-redis:
|
||||
appwrite-cache:
|
||||
|
||||
+42
-552
@@ -1,586 +1,69 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/init.php';
|
||||
$registerWorkerMessageResources = require __DIR__ . '/init/worker/message.php';
|
||||
|
||||
use Appwrite\Certificates\LetsEncrypt;
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Screenshot;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Executor\Executor;
|
||||
use Swoole\Runtime;
|
||||
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
|
||||
use Utopia\Audit\Adapter\Database as AdapterDatabase;
|
||||
use Utopia\Audit\Audit as UtopiaAudit;
|
||||
use Utopia\Cache\Adapter\Pool as CachePool;
|
||||
use Utopia\Cache\Adapter\Sharding;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Adapter\Pool as DatabasePool;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Platform\Service;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Queue\Adapter\Swoole;
|
||||
use Utopia\Queue\Broker\Pool as BrokerPool;
|
||||
use Utopia\Queue\Message;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Queue\Queue;
|
||||
use Utopia\Queue\Server;
|
||||
use Utopia\Registry\Registry;
|
||||
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter as Telemetry;
|
||||
use Utopia\Telemetry\Adapter\None as NoTelemetry;
|
||||
|
||||
Runtime::enableCoroutine();
|
||||
require_once __DIR__ . '/init/span.php';
|
||||
|
||||
global $register;
|
||||
Server::setResource('register', fn () => $register);
|
||||
global $container;
|
||||
$container->set('pools', function ($register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
|
||||
Server::setResource('authorization', function () {
|
||||
$container->set('authorization', function () {
|
||||
$authorization = new Authorization();
|
||||
$authorization->disable();
|
||||
|
||||
return $authorization;
|
||||
}, []);
|
||||
|
||||
Server::setResource('dbForPlatform', function (Cache $cache, Registry $register, Authorization $authorization) {
|
||||
$pools = $register->get('pools');
|
||||
$adapter = new DatabasePool($pools->get('console'));
|
||||
$dbForPlatform = new Database($adapter, $cache);
|
||||
$container->set('project', fn () => new Document([]), []);
|
||||
|
||||
$dbForPlatform
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setNamespace('_console')
|
||||
->setDocumentType('users', User::class);
|
||||
$container->set('log', fn () => new Log(), []);
|
||||
|
||||
return $dbForPlatform;
|
||||
}, ['cache', 'register', 'authorization']);
|
||||
|
||||
Server::setResource('project', function (Message $message, Database $dbForPlatform) {
|
||||
$payload = $message->getPayload() ?? [];
|
||||
$project = new Document($payload['project'] ?? []);
|
||||
|
||||
if ($project->getId() === 'console') {
|
||||
return $project;
|
||||
}
|
||||
|
||||
return $dbForPlatform->getDocument('projects', $project->getId());
|
||||
}, ['message', 'dbForPlatform']);
|
||||
|
||||
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForPlatform, Authorization $authorization) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
}
|
||||
|
||||
$pools = $register->get('pools');
|
||||
|
||||
try {
|
||||
$dsn = new DSN($project->getAttribute('database'));
|
||||
} catch (\InvalidArgumentException) {
|
||||
// TODO: Temporary until all projects are using shared tables
|
||||
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
|
||||
}
|
||||
|
||||
$adapter = new DatabasePool($pools->get($dsn->getHost()));
|
||||
$database = new Database($adapter, $cache);
|
||||
$database->setDocumentType('users', User::class);
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
|
||||
return $database;
|
||||
}, ['cache', 'register', 'message', 'project', 'dbForPlatform', 'authorization']);
|
||||
|
||||
Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) {
|
||||
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
|
||||
|
||||
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
}
|
||||
|
||||
try {
|
||||
$dsn = new DSN($project->getAttribute('database'));
|
||||
} catch (\InvalidArgumentException) {
|
||||
// TODO: Temporary until all projects are using shared tables
|
||||
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
|
||||
}
|
||||
|
||||
if (isset($databases[$dsn->getHost()])) {
|
||||
$database = $databases[$dsn->getHost()];
|
||||
$database->setAuthorization($authorization);
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
return $database;
|
||||
}
|
||||
|
||||
$adapter = new DatabasePool($pools->get($dsn->getHost()));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
$databases[$dsn->getHost()] = $database;
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
|
||||
|
||||
Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
$database = null;
|
||||
|
||||
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
|
||||
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant($project->getSequence());
|
||||
return $database;
|
||||
}
|
||||
|
||||
$adapter = new DatabasePool($pools->get('logs'));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setSharedTables(true)
|
||||
->setNamespace('logsV1')
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
|
||||
|
||||
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant($project->getSequence());
|
||||
}
|
||||
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) {
|
||||
$initializedPools = [];
|
||||
|
||||
return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization, &$initializedPools): Database {
|
||||
$projectDocument ??= $project;
|
||||
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
|
||||
$databaseType = $database->getAttribute('type', '');
|
||||
|
||||
// Backwards‑compatibility: older or seeded legacy databases may not have a DSN stored
|
||||
// in the "database" attribute. In that case, fall back to the project's database DSN.
|
||||
if ($databaseDSN === '') {
|
||||
$databaseDSN = $projectDocument->getAttribute('database', '');
|
||||
}
|
||||
|
||||
try {
|
||||
$databaseDSN = new DSN($databaseDSN);
|
||||
} catch (\InvalidArgumentException) {
|
||||
$databaseDSN = new DSN('mysql://'.$databaseDSN);
|
||||
}
|
||||
|
||||
try {
|
||||
$dsn = new DSN($projectDocument->getAttribute('database'));
|
||||
} catch (\InvalidArgumentException) {
|
||||
// Temporary fallback until all projects use shared tables
|
||||
$dsn = new DSN('mysql://' . $projectDocument->getAttribute('database'));
|
||||
}
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$databaseHost = $databaseDSN->getHost();
|
||||
$pool = $pools->get($databaseHost);
|
||||
|
||||
$adapter = new DatabasePool($pool);
|
||||
$database = new Database($adapter, $cache);
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization);
|
||||
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
|
||||
|
||||
$sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')));
|
||||
|
||||
// When using a separate pool, always use dedicated mode with namespace isolation.
|
||||
// Shared tables mode can't be used across different engines (e.g. MongoDB UUID
|
||||
// tenants are incompatible with PostgreSQL's integer _tenant column).
|
||||
if ($databaseHost !== $dsn->getHost()) {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $projectDocument->getSequence());
|
||||
|
||||
$poolKey = $databaseHost . ':' . $database->getNamespace();
|
||||
if (!isset($initializedPools[$poolKey])) {
|
||||
try {
|
||||
$database->create();
|
||||
} catch (\Utopia\Database\Exception\Duplicate) {
|
||||
// Schema already exists
|
||||
}
|
||||
$initializedPools[$poolKey] = true;
|
||||
}
|
||||
} elseif (\in_array($dsn->getHost(), $sharedTables, true)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($projectDocument->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $projectDocument->getSequence());
|
||||
}
|
||||
|
||||
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
return $database;
|
||||
};
|
||||
}, ['cache', 'register', 'project', 'authorization']);
|
||||
|
||||
Server::setResource('abuseRetention', function () {
|
||||
return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day
|
||||
});
|
||||
|
||||
Server::setResource('auditRetention', function (Document $project) {
|
||||
if ($project->getId() === 'console') {
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
|
||||
}
|
||||
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
|
||||
}, ['project']);
|
||||
|
||||
Server::setResource('executionRetention', function () {
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
|
||||
});
|
||||
|
||||
Server::setResource('cache', function (Registry $register) {
|
||||
$pools = $register->get('pools');
|
||||
$list = Config::getParam('pools-cache', []);
|
||||
$adapters = [];
|
||||
|
||||
foreach ($list as $value) {
|
||||
$adapters[] = new CachePool($pools->get($value));
|
||||
}
|
||||
|
||||
return new Cache(new Sharding($adapters));
|
||||
}, ['register']);
|
||||
|
||||
Server::setResource('redis', function () {
|
||||
$host = System::getEnv('_APP_REDIS_HOST', 'localhost');
|
||||
$port = System::getEnv('_APP_REDIS_PORT', 6379);
|
||||
$pass = System::getEnv('_APP_REDIS_PASS', '');
|
||||
|
||||
$redis = new \Redis();
|
||||
@$redis->pconnect($host, (int) $port);
|
||||
if ($pass) {
|
||||
$redis->auth($pass);
|
||||
}
|
||||
$redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
|
||||
|
||||
return $redis;
|
||||
});
|
||||
|
||||
Server::setResource('timelimit', function (\Redis $redis) {
|
||||
return function (string $key, int $limit, int $time) use ($redis) {
|
||||
return new TimeLimitRedis($key, $limit, $time, $redis);
|
||||
};
|
||||
}, ['redis']);
|
||||
|
||||
Server::setResource('log', fn () => new Log());
|
||||
|
||||
Server::setResource('publisher', function (Group $pools) {
|
||||
return new BrokerPool(publisher: $pools->get('publisher'));
|
||||
}, ['pools']);
|
||||
|
||||
Server::setResource('publisherDatabases', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('publisherFunctions', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('publisherMigrations', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('publisherMessaging', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('consumer', function (Group $pools) {
|
||||
$container->set('consumer', function (Group $pools) {
|
||||
return new BrokerPool(consumer: $pools->get('consumer'));
|
||||
}, ['pools']);
|
||||
|
||||
Server::setResource('consumerDatabases', function (BrokerPool $consumer) {
|
||||
$container->set('consumerDatabases', function (BrokerPool $consumer) {
|
||||
return $consumer;
|
||||
}, ['consumer']);
|
||||
|
||||
Server::setResource('consumerMigrations', function (BrokerPool $consumer) {
|
||||
$container->set('consumerMigrations', function (BrokerPool $consumer) {
|
||||
return $consumer;
|
||||
}, ['consumer']);
|
||||
|
||||
Server::setResource('consumerStatsUsage', function (BrokerPool $consumer) {
|
||||
$container->set('consumerStatsUsage', function (BrokerPool $consumer) {
|
||||
return $consumer;
|
||||
}, ['consumer']);
|
||||
|
||||
Server::setResource('usage', function () {
|
||||
return new Context();
|
||||
}, []);
|
||||
Server::setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
|
||||
Server::setResource('queueForDatabase', function (Publisher $publisher) {
|
||||
return new EventDatabase($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForMessaging', function (Publisher $publisher) {
|
||||
return new Messaging($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForMails', function (Publisher $publisher) {
|
||||
return new Mail($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForBuilds', function (Publisher $publisher) {
|
||||
return new Build($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForScreenshots', function (Publisher $publisher) {
|
||||
return new Screenshot($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForDeletes', function (Publisher $publisher) {
|
||||
return new Delete($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForEvents', function (Publisher $publisher) {
|
||||
return new Event($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForAudits', function (Publisher $publisher) {
|
||||
return new Audit($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForWebhooks', function (Publisher $publisher) {
|
||||
return new Webhook($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForFunctions', function (Publisher $publisher) {
|
||||
return new Func($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForRealtime', function () {
|
||||
return new Realtime();
|
||||
}, []);
|
||||
|
||||
Server::setResource('queueForCertificates', function (Publisher $publisher) {
|
||||
return new Certificate($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForMigrations', function (Publisher $publisher) {
|
||||
return new Migration($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('logger', function (Registry $register) {
|
||||
return $register->get('logger');
|
||||
}, ['register']);
|
||||
|
||||
Server::setResource('pools', function (Registry $register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
|
||||
Server::setResource('telemetry', fn () => new NoTelemetry());
|
||||
|
||||
Server::setResource('deviceForSites', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
Server::setResource('deviceForMigrations', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
Server::setResource('deviceForFunctions', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
Server::setResource('deviceForFiles', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
Server::setResource('deviceForBuilds', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
Server::setResource('deviceForCache', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
Server::setResource(
|
||||
'isResourceBlocked',
|
||||
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
|
||||
);
|
||||
|
||||
Server::setResource('plan', function (array $plan = []) {
|
||||
return [];
|
||||
});
|
||||
|
||||
Server::setResource('certificates', function () {
|
||||
$container->set('certificates', function () {
|
||||
$email = System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'));
|
||||
if (empty($email)) {
|
||||
throw new Exception('You must set a valid security email address (_APP_EMAIL_CERTIFICATES) to issue a LetsEncrypt SSL certificate.');
|
||||
}
|
||||
|
||||
return new LetsEncrypt($email);
|
||||
});
|
||||
}, []);
|
||||
|
||||
Server::setResource('logError', function (Registry $register, Document $project) {
|
||||
return function (Throwable $error, string $namespace, string $action, ?array $extras = null) use ($register, $project) {
|
||||
$logger = $register->get('logger');
|
||||
|
||||
if ($logger) {
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
$log = new Log();
|
||||
$log->setNamespace($namespace);
|
||||
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
$log->addTag('projectId', $project->getId() ?? '');
|
||||
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
|
||||
if ($error->getPrevious() !== null) {
|
||||
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
|
||||
$log->addExtra('previousMessage', $error->getPrevious()->getMessage());
|
||||
}
|
||||
$log->addExtra('previousFile', $error->getPrevious()->getFile());
|
||||
$log->addExtra('previousLine', $error->getPrevious()->getLine());
|
||||
}
|
||||
|
||||
foreach (($extras ?? []) as $key => $value) {
|
||||
$log->addExtra($key, $value);
|
||||
}
|
||||
|
||||
$log->setAction($action);
|
||||
|
||||
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
try {
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Error log pushed with status code: ' . $responseCode);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Error pushing log: ' . $th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
Console::warning("Failed: {$error->getMessage()}");
|
||||
Console::warning($error->getTraceAsString());
|
||||
|
||||
if ($error->getPrevious() !== null) {
|
||||
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
|
||||
Console::warning("Previous Failed: {$error->getPrevious()->getMessage()}");
|
||||
}
|
||||
Console::warning("Previous File: {$error->getPrevious()->getFile()} Line: {$error->getPrevious()->getLine()}");
|
||||
}
|
||||
};
|
||||
}, ['register', 'project']);
|
||||
|
||||
Server::setResource('executor', fn () => new Executor());
|
||||
|
||||
Server::setResource('getAudit', function (Database $dbForPlatform, callable $getProjectDB) {
|
||||
return function (Document $project) use ($dbForPlatform, $getProjectDB) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
$adapter = new AdapterDatabase($dbForPlatform);
|
||||
|
||||
return new UtopiaAudit($adapter);
|
||||
}
|
||||
|
||||
$dbForProject = $getProjectDB($project);
|
||||
$adapter = new AdapterDatabase($dbForProject);
|
||||
|
||||
return new UtopiaAudit($adapter);
|
||||
};
|
||||
}, ['dbForPlatform', 'getProjectDB']);
|
||||
|
||||
Server::setResource('executionsRetentionCount', function (Document $project, array $plan) {
|
||||
if ($project->getId() === 'console' || empty($plan)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) ($plan['executionsRetentionCount'] ?? 100);
|
||||
}, ['project', 'plan']);
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$platform = new Appwrite();
|
||||
$args = $platform->getEnv('argv');
|
||||
$args = $_SERVER['argv'] ?? [];
|
||||
|
||||
if (! isset($args[1])) {
|
||||
Console::error('Missing worker name');
|
||||
@@ -596,38 +79,45 @@ if (\str_starts_with($workerName, 'databases')) {
|
||||
$queueName = System::getEnv('_APP_QUEUE_NAME', 'v1-' . strtolower($workerName));
|
||||
}
|
||||
|
||||
/** @var \Utopia\Pools\Group $pools */
|
||||
$pools = $container->get('pools');
|
||||
|
||||
$adapter = new Swoole(
|
||||
$pools->get('consumer')->pop()->getResource(),
|
||||
System::getEnv('_APP_WORKERS_NUM', 1),
|
||||
$queueName
|
||||
);
|
||||
|
||||
$worker = new Server($adapter, $container);
|
||||
|
||||
try {
|
||||
/**
|
||||
* Any worker can be configured with the following env vars:
|
||||
* - _APP_WORKERS_NUM The total number of worker processes
|
||||
* - _APP_WORKER_PER_CORE The number of worker processes per core (ignored if _APP_WORKERS_NUM is set)
|
||||
* - _APP_QUEUE_NAME The name of the queue to read for database events
|
||||
*/
|
||||
$worker->init()->action(function () use ($worker, $registerWorkerMessageResources) {
|
||||
$registerWorkerMessageResources($worker->getContainer());
|
||||
});
|
||||
|
||||
$container->set('bus', function ($register) use ($worker) {
|
||||
return $register->get('bus')->setResolver(
|
||||
fn (string $name) => $worker->getContainer()->get($name)
|
||||
);
|
||||
}, ['register']);
|
||||
|
||||
$platform->setWorker($worker);
|
||||
$platform->init(Service::TYPE_WORKER, [
|
||||
'workersNum' => System::getEnv('_APP_WORKERS_NUM', 1),
|
||||
'connection' => $pools->get('consumer')->pop()->getResource(),
|
||||
'workerName' => strtolower($workerName) ?? null,
|
||||
'queueName' => $queueName,
|
||||
'workerName' => strtolower($workerName),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine());
|
||||
Console::exit(1);
|
||||
}
|
||||
|
||||
$worker = $platform->getWorker();
|
||||
|
||||
Server::setResource('bus', function ($register) use ($worker) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $worker->getResource($name));
|
||||
}, ['register']);
|
||||
|
||||
$worker
|
||||
->error()
|
||||
->inject('error')
|
||||
->inject('logger')
|
||||
->inject('log')
|
||||
->inject('pools')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project, Authorization $authorization) use ($queueName) {
|
||||
->action(function (Throwable $error, ?Logger $logger, Log $log, Document $project, Authorization $authorization) use ($queueName) {
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
if ($logger) {
|
||||
@@ -639,7 +129,7 @@ $worker
|
||||
$log->setAction('appwrite-queue-' . $queueName);
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('projectId', $project->getId() ?? 'n/a');
|
||||
$log->addTag('projectId', $project->getId());
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
|
||||
+9
-10
@@ -52,34 +52,34 @@
|
||||
"appwrite/php-runtimes": "0.19.*",
|
||||
"appwrite/php-clamav": "2.0.*",
|
||||
"utopia-php/abuse": "1.2.*",
|
||||
"utopia-php/agents": "1.2.*",
|
||||
"utopia-php/analytics": "0.15.*",
|
||||
"utopia-php/audit": "2.2.*",
|
||||
"utopia-php/auth": "0.5.*",
|
||||
"utopia-php/cache": "1.0.*",
|
||||
"utopia-php/cli": "0.22.*",
|
||||
"utopia-php/cli": "0.23.*",
|
||||
"utopia-php/compression": "0.1.*",
|
||||
"utopia-php/config": "1.*",
|
||||
"utopia-php/console": "0.1.*",
|
||||
"utopia-php/database": "5.*",
|
||||
"utopia-php/agents": "1.*",
|
||||
"utopia-php/detector": "0.2.*",
|
||||
"utopia-php/domains": "1.*",
|
||||
"utopia-php/emails": "0.6.*",
|
||||
"utopia-php/dns": "1.6.*",
|
||||
"utopia-php/dsn": "0.2.1",
|
||||
"utopia-php/framework": "0.33.*",
|
||||
"utopia-php/http": "0.34.*",
|
||||
"utopia-php/fetch": "0.5.*",
|
||||
"utopia-php/image": "0.8.*",
|
||||
"utopia-php/locale": "0.8.*",
|
||||
"utopia-php/logger": "0.6.*",
|
||||
"utopia-php/messaging": "0.20.*",
|
||||
"utopia-php/messaging": "0.22.*",
|
||||
"utopia-php/migration": "1.9.*",
|
||||
"utopia-php/platform": "0.7.*",
|
||||
"utopia-php/platform": "0.13.*",
|
||||
"utopia-php/pools": "1.*",
|
||||
"utopia-php/span": "1.1.*",
|
||||
"utopia-php/preloader": "0.2.*",
|
||||
"utopia-php/queue": "0.15.*",
|
||||
"utopia-php/servers": "0.2.5",
|
||||
"utopia-php/queue": "0.17.*",
|
||||
"utopia-php/servers": "0.3.*",
|
||||
"utopia-php/registry": "0.5.*",
|
||||
"utopia-php/storage": "1.0.*",
|
||||
"utopia-php/system": "0.10.*",
|
||||
@@ -92,10 +92,9 @@
|
||||
"chillerlan/php-qrcode": "4.3.*",
|
||||
"adhocore/jwt": "1.1.*",
|
||||
"spomky-labs/otphp": "11.*",
|
||||
"webonyx/graphql-php": "14.11.*",
|
||||
"webonyx/graphql-php": "15.31.*",
|
||||
"league/csv": "9.14.*",
|
||||
"enshrined/svg-sanitize": "0.22.*",
|
||||
"utopia-php/di": "0.1.0"
|
||||
"enshrined/svg-sanitize": "0.22.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-fileinfo": "*",
|
||||
|
||||
Generated
+257
-249
File diff suppressed because it is too large
Load Diff
+2
-3
@@ -242,7 +242,6 @@ services:
|
||||
- _APP_EXPERIMENT_LOGGING_PROVIDER
|
||||
- _APP_EXPERIMENT_LOGGING_CONFIG
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
- _APP_DATABASE_SHARED_TABLES_V1
|
||||
- _APP_DATABASE_SHARED_NAMESPACE
|
||||
- _APP_FUNCTIONS_CREATION_ABUSE_LIMIT
|
||||
- _APP_CUSTOM_DOMAIN_DENY_LIST
|
||||
@@ -254,7 +253,7 @@ services:
|
||||
appwrite-console:
|
||||
<<: *x-logging
|
||||
container_name: appwrite-console
|
||||
image: appwrite/console:7.8.26
|
||||
image: appwrite/console:7.8.45
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
@@ -462,7 +461,6 @@ services:
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_EXECUTOR_HOST
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
- _APP_DATABASE_SHARED_TABLES_V1
|
||||
- _APP_EMAIL_CERTIFICATES
|
||||
- _APP_MAINTENANCE_RETENTION_AUDIT
|
||||
- _APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE
|
||||
@@ -1288,6 +1286,7 @@ services:
|
||||
image: mongo:8.2.5
|
||||
container_name: appwrite-mongodb
|
||||
<<: *x-logging
|
||||
restart: on-failure:3
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Initialize an MFA challenge of the specified factor. The factor must be available on the account.
|
||||
@@ -1 +0,0 @@
|
||||
Use this endpoint to log out the currently logged in user from their account. When successful this endpoint will delete the user session and remove the session secret cookie from the user client.
|
||||
@@ -1 +0,0 @@
|
||||
Get the collection activity logs list by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get the document activity logs list by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
List attributes in the collection.
|
||||
@@ -1 +0,0 @@
|
||||
Create a new build for an existing function deployment. This endpoint allows you to rebuild a deployment with the updated function configuration, including its entrypoint and build commands if they have been modified. The build process will be queued and executed asynchronously. The original deployment's code will be preserved and used for the new build.
|
||||
@@ -1,5 +0,0 @@
|
||||
Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.
|
||||
|
||||
This endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https://appwrite.io/docs/functions).
|
||||
|
||||
Use the "command" param to set the entrypoint used to execute your code.
|
||||
@@ -1 +0,0 @@
|
||||
Trigger a function execution. The returned object will return you the current execution status. You can ping the `Get Execution` endpoint to get updates on the current execution status. Once this endpoint is called, your function execution process will start asynchronously.
|
||||
@@ -1 +0,0 @@
|
||||
Create a new function. You can pass a list of [permissions](https://appwrite.io/docs/permissions) to allow different project users or team with access to execute the function using the client API.
|
||||
@@ -1 +0,0 @@
|
||||
Create a new function environment variable. These variables can be accessed in the function at runtime as environment variables.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a code deployment by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a function execution by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a function by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a variable by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get a Deployment's contents by its unique ID. This endpoint supports range requests for partial or streaming file download.
|
||||
@@ -1 +0,0 @@
|
||||
Get a code deployment by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get a function execution log by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get usage metrics and statistics for a for a specific function. View statistics including total deployments, builds, executions, storage usage, and compute time. The response includes both current totals and historical data for each metric. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, defaults to 30 days.
|
||||
@@ -1 +0,0 @@
|
||||
Get a function by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get usage metrics and statistics for a for all functions. View statistics including total functions, deployments, builds, executions, storage usage, and compute time. The response includes both current totals and historical data for each metric. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, defaults to 30 days.
|
||||
@@ -1 +0,0 @@
|
||||
Get a function template using ID. You can use template details in [createFunction](/docs/references/cloud/server-nodejs/functions#create) method.
|
||||
@@ -1 +0,0 @@
|
||||
Get a variable by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get a list of all the function's code deployments. You can use the query params to filter your results.
|
||||
@@ -1 +0,0 @@
|
||||
Get a list of all the current user function execution logs. You can use the query params to filter your results.
|
||||
@@ -1 +0,0 @@
|
||||
Get a list of all the project's functions. You can use the query params to filter your results.
|
||||
@@ -1 +0,0 @@
|
||||
Get a list of all runtimes that are currently active on your instance.
|
||||
@@ -1 +0,0 @@
|
||||
List allowed function specifications for this instance.
|
||||
@@ -1 +0,0 @@
|
||||
List available function templates. You can use template details in [createFunction](/docs/references/cloud/server-nodejs/functions#create) method.
|
||||
@@ -1 +0,0 @@
|
||||
Get a list of all variables of a specific function.
|
||||
@@ -1 +0,0 @@
|
||||
Cancel an ongoing function deployment build. If the build is already in progress, it will be stopped and marked as canceled. If the build hasn't started yet, it will be marked as canceled without executing. You cannot cancel builds that have already completed (status 'ready') or failed. The response includes the final build status and details.
|
||||
@@ -1 +0,0 @@
|
||||
Update the function code deployment ID using the unique function ID. Use this endpoint to switch the code deployment that should be executed by the execution endpoint.
|
||||
@@ -1 +0,0 @@
|
||||
Update function by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Update variable by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get the number of projects containing metrics that are waiting to be processed in the Appwrite internal queue server.
|
||||
@@ -1 +0,0 @@
|
||||
Get the number of tasks that are waiting to be processed in the Appwrite internal queue server.
|
||||
@@ -1 +0,0 @@
|
||||
Check the Appwrite queue messaging servers are up and connection is successful.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a message by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Export documents to a JSON file from your Appwrite database. This endpoint allows you to export documents to a JSON file stored in a secure internal bucket. You'll receive an email with a download link when the export is complete.
|
||||
@@ -0,0 +1 @@
|
||||
Import documents from a JSON file into your Appwrite database. This endpoint allows you to import documents from a JSON file uploaded to Appwrite Storage bucket.
|
||||
@@ -1 +0,0 @@
|
||||
Create a new project variable. This variable will be accessible in all Appwrite Functions at runtime.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a project variable by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get a project variable by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get a list of all project variables. These variables will be accessible in all Appwrite Functions at runtime.
|
||||
@@ -1 +0,0 @@
|
||||
Update project variable by its unique ID. This variable will be accessible in all Appwrite Functions at runtime.
|
||||
@@ -1 +0,0 @@
|
||||
Create a new API key. It's recommended to have multiple API keys with strict scopes for separate functions within your project.
|
||||
@@ -1 +0,0 @@
|
||||
Create a new platform for your project. Use this endpoint to register a new platform where your users will run your application which will interact with the Appwrite API.
|
||||
@@ -1 +0,0 @@
|
||||
Create a new webhook. Use this endpoint to configure a URL that will receive events from Appwrite when specific events occur.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a key by its unique ID. Once deleted, the key can no longer be used to authenticate API calls.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a platform by its unique ID. This endpoint removes the platform and all its configurations from the project.
|
||||
@@ -1 +0,0 @@
|
||||
Reset a custom SMS template to its default value. This endpoint removes any custom message and restores the template to its original state.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a webhook by its unique ID. Once deleted, the webhook will no longer receive project events.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user