mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
691
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a229cc1367 | ||
|
|
2dce141d17 | ||
|
|
d421b5ff30 | ||
|
|
b8eb0810c2 | ||
|
|
cb74a5756a | ||
|
|
ba25849871 | ||
|
|
44f3bbae03 | ||
|
|
9ac686ae31 | ||
|
|
7385bc1d9f | ||
|
|
e3f373b8d2 | ||
|
|
8b591f3522 | ||
|
|
4e10cabec0 | ||
|
|
5d1da00138 | ||
|
|
5ab28ad99a | ||
|
|
9be447aacf | ||
|
|
66e68aea14 | ||
|
|
cf8519bfa5 | ||
|
|
860cf8cd57 | ||
|
|
c978b6f34f | ||
|
|
f3f2855fe5 | ||
|
|
3cb53f0604 | ||
|
|
a5f45b46e9 | ||
|
|
130c2221ec | ||
|
|
c08e5f706c | ||
|
|
58a90cca4a | ||
|
|
90e705f8c5 | ||
|
|
17076e4a00 | ||
|
|
7c50bbc500 | ||
|
|
387668bb47 | ||
|
|
b6e95f4502 | ||
|
|
1900492aad | ||
|
|
96f4ef7d22 | ||
|
|
8e02a5a227 | ||
|
|
c2dd8aceda | ||
|
|
24fcf4247c | ||
|
|
2dc20ef0eb | ||
|
|
04943b6313 | ||
|
|
eb366cf94b | ||
|
|
77b4f8b7a0 | ||
|
|
24bd4c3d7b | ||
|
|
33f8e35b62 | ||
|
|
2c1060f57a | ||
|
|
30e0ca81bd | ||
|
|
92cc382a6b | ||
|
|
7d428ffe83 | ||
|
|
fb96aecbef | ||
|
|
80d5d4716a | ||
|
|
c1dde09070 | ||
|
|
d531c29fc8 | ||
|
|
7044601603 | ||
|
|
53bc126015 | ||
|
|
f1dc468e50 | ||
|
|
ce63b00cf4 | ||
|
|
b9aaecba25 | ||
|
|
d9c606a1c2 | ||
|
|
a5b0378138 | ||
|
|
320068a576 | ||
|
|
3cd90ae629 | ||
|
|
44f33473bd | ||
|
|
358f1b78a8 | ||
|
|
1f6b9d94bf | ||
|
|
a734c8cd46 | ||
|
|
1788e1bd6c | ||
|
|
983adf3ffd | ||
|
|
f2ea0b9b48 | ||
|
|
44610462b3 | ||
|
|
28ece7de02 | ||
|
|
8c6d4d8b36 | ||
|
|
2ebc6f70ef | ||
|
|
2b7690d6db | ||
|
|
d9af799cc7 | ||
|
|
3ed1ca736d | ||
|
|
9fa35db838 | ||
|
|
b6e020389b | ||
|
|
ccc0cfbfdc | ||
|
|
b91506fc2d | ||
|
|
9ffc23946c | ||
|
|
8bb6b5cd2a | ||
|
|
afea4ca57b | ||
|
|
b30b89ade9 | ||
|
|
829cf887dc | ||
|
|
d862a64874 | ||
|
|
142e671c7d | ||
|
|
b4085d1083 | ||
|
|
18ed6a9c59 | ||
|
|
3d64ccd056 | ||
|
|
4f73eb021f | ||
|
|
7dcfd3ff3d | ||
|
|
4dfdfb5e59 | ||
|
|
5d1009b324 | ||
|
|
4568316345 | ||
|
|
c444bccacc | ||
|
|
f9aee4de5d | ||
|
|
037878cc75 | ||
|
|
416b26ac7d | ||
|
|
b47ac00ca8 | ||
|
|
2f53d09c5b | ||
|
|
1fa1aa8621 | ||
|
|
ec20fb59e2 | ||
|
|
7e49cf0bed | ||
|
|
4e32497be1 | ||
|
|
fe6f79c9c6 | ||
|
|
5c84efdd7c | ||
|
|
02d234ad3a | ||
|
|
8b7459f634 | ||
|
|
100cbf50f0 | ||
|
|
c7c59f4e7b | ||
|
|
a955dff55a | ||
|
|
7bcd5b4ebc | ||
|
|
dfb23612b0 | ||
|
|
4e71a54fae | ||
|
|
ea032c2804 | ||
|
|
a80ecd0cb6 | ||
|
|
8be7c6182e | ||
|
|
3bb6a8bcc8 | ||
|
|
c72d438a10 | ||
|
|
0085d93aeb | ||
|
|
3b9b0c96c4 | ||
|
|
2611bf4af1 | ||
|
|
de219de31d | ||
|
|
d8bbd82556 | ||
|
|
5d962e21b0 | ||
|
|
551c83dd2c | ||
|
|
d964c5a439 | ||
|
|
4850fb54ed | ||
|
|
796bc13c19 | ||
|
|
30e080c4d3 | ||
|
|
aaebeec61e | ||
|
|
1789d2ce89 | ||
|
|
239a9c5457 | ||
|
|
995e9fdf39 | ||
|
|
8ce42fe057 | ||
|
|
c14ebab1a0 | ||
|
|
f1ea496764 | ||
|
|
95b6da085b | ||
|
|
32005c0a49 | ||
|
|
98c63a87d1 | ||
|
|
c5b9ac3dba | ||
|
|
be50318e5d | ||
|
|
f901b6e0ac | ||
|
|
53d0e14f97 | ||
|
|
325b1d67b4 | ||
|
|
3ff7cadcab | ||
|
|
f2c8500d73 | ||
|
|
babb0196e4 | ||
|
|
9353fbfb74 | ||
|
|
918f1d37d4 | ||
|
|
196acd45a2 | ||
|
|
ef6fdc6c72 | ||
|
|
915a8bfbe0 | ||
|
|
741267c6c5 | ||
|
|
e85080499a | ||
|
|
ee1ca5ace6 | ||
|
|
b36472f0da | ||
|
|
52ae8b3880 | ||
|
|
8f09e74462 | ||
|
|
30907d716f | ||
|
|
eb46855e72 | ||
|
|
45557e5929 | ||
|
|
6ad6a5dea3 | ||
|
|
098f7aa3e3 | ||
|
|
f8c8c17757 | ||
|
|
5b1ee93927 | ||
|
|
5fbaa7ab6e | ||
|
|
e8a0a895ed | ||
|
|
560ebeaddd | ||
|
|
0731cc1535 | ||
|
|
983d0b8fad | ||
|
|
92423acc01 | ||
|
|
42414a46b0 | ||
|
|
b7bd86d634 | ||
|
|
7a39da0afd | ||
|
|
9aa488c961 | ||
|
|
cfc325635d | ||
|
|
5c47d4f48b | ||
|
|
7aff75ae1c | ||
|
|
651a24a211 | ||
|
|
6536463d49 | ||
|
|
82d7926c4b | ||
|
|
6041468fc4 | ||
|
|
669f323156 | ||
|
|
1ab05acc93 | ||
|
|
2f66ae5533 | ||
|
|
87c462ebd4 | ||
|
|
f89621cf74 | ||
|
|
4fb8d873b1 | ||
|
|
c5186324ed | ||
|
|
b95b4f12f9 | ||
|
|
52efca0832 | ||
|
|
ee3d4dfeee | ||
|
|
b742f1c50d | ||
|
|
f3fcdec1be | ||
|
|
647099efc7 | ||
|
|
d535b6b39b | ||
|
|
2057eab918 | ||
|
|
f89b3274de | ||
|
|
956edae593 | ||
|
|
617d6fe1eb | ||
|
|
a7cdfed253 | ||
|
|
dc6b2ce3aa | ||
|
|
7fcc640652 | ||
|
|
10a6e8832b | ||
|
|
74adda8e77 | ||
|
|
a92ad3aa8b | ||
|
|
4de9ec7fba | ||
|
|
a659038ad2 | ||
|
|
9564c9b065 | ||
|
|
4da726029c | ||
|
|
f016d4b7cd | ||
|
|
2a7925b362 | ||
|
|
cfe600c653 | ||
|
|
22e1969895 | ||
|
|
49688a6c75 | ||
|
|
d0978d891f | ||
|
|
0cf206cacf | ||
|
|
b9b5d396b8 | ||
|
|
2b33dc3c72 | ||
|
|
9e595588bc | ||
|
|
20bd7af370 | ||
|
|
5ca30d37f7 | ||
|
|
76684874e9 | ||
|
|
4fffeda596 | ||
|
|
1acfef5f5d | ||
|
|
d4f7d51df8 | ||
|
|
5b5020fda4 | ||
|
|
a1441174f2 | ||
|
|
e58f0b6378 | ||
|
|
c5c78e0f2b | ||
|
|
2e5d7a53eb | ||
|
|
60b5f4433c | ||
|
|
56ce0d4b47 | ||
|
|
8666bf1325 | ||
|
|
d53cad2b0f | ||
|
|
9d976fdc9f | ||
|
|
ce5d5cf6ef | ||
|
|
2017183ecc | ||
|
|
995deea801 | ||
|
|
1a1740ac7a | ||
|
|
0114e260f0 | ||
|
|
7ab474b963 | ||
|
|
10da066075 | ||
|
|
01142bba2c | ||
|
|
71146118d2 | ||
|
|
682105c068 | ||
|
|
07ff923d38 | ||
|
|
2c5e029116 | ||
|
|
336bc3a74f | ||
|
|
8a80ec12ed | ||
|
|
b7f06a76aa | ||
|
|
a9acba916a | ||
|
|
fa97823385 | ||
|
|
8ae07ac61f | ||
|
|
ffbe17f34e | ||
|
|
5466f55cfa | ||
|
|
6c8338c3e9 | ||
|
|
28ba11c71e | ||
|
|
a14d51321a | ||
|
|
d8f3d581cc | ||
|
|
be76990bb6 | ||
|
|
6e59e1307d | ||
|
|
34f23bdc9f | ||
|
|
24848a872c | ||
|
|
b630426893 | ||
|
|
0a5a8c5b6c | ||
|
|
875637bf35 | ||
|
|
1aa86708f3 | ||
|
|
6410c2dcdf | ||
|
|
3a40b6b629 | ||
|
|
ef0954cdda | ||
|
|
d4c9af2eb2 | ||
|
|
f74a60dba4 | ||
|
|
3baf2681df | ||
|
|
1ad2cd68ef | ||
|
|
04680d68f5 | ||
|
|
9a70445395 | ||
|
|
dabe98d4a1 | ||
|
|
c9d023991d | ||
|
|
91e252382b | ||
|
|
47408f04dc | ||
|
|
827dd2d039 | ||
|
|
ed1ab85356 | ||
|
|
c155c88a31 | ||
|
|
df58831d92 | ||
|
|
aef9f447f7 | ||
|
|
48e99e70ac | ||
|
|
2007fbf241 | ||
|
|
2673af7b83 | ||
|
|
21d7e2dfb5 | ||
|
|
9235a4cc15 | ||
|
|
fb21211ae8 | ||
|
|
616762f696 | ||
|
|
3b5ba2a2b0 | ||
|
|
7ae1cf4db0 | ||
|
|
c537d09b5c | ||
|
|
efeb3b4a00 | ||
|
|
aa89128d10 | ||
|
|
e8c0ab0cc1 | ||
|
|
b81f3f8267 | ||
|
|
b79132556c | ||
|
|
99c76689ee | ||
|
|
c7907932e4 | ||
|
|
bdd3c2f9f5 | ||
|
|
1754d6cc81 | ||
|
|
351efa6cf2 | ||
|
|
1aa2c7ff2b | ||
|
|
9917f95dfd | ||
|
|
8af0efafd4 | ||
|
|
7e7cac017c | ||
|
|
b8cb146eb9 | ||
|
|
9ed6ac89f5 | ||
|
|
09313d0500 | ||
|
|
3a9e4305d3 | ||
|
|
4b163c02ad | ||
|
|
331fb7f689 | ||
|
|
b80d76e287 | ||
|
|
feddd77066 | ||
|
|
a55f3e1db8 | ||
|
|
9bc263ab22 | ||
|
|
6c76783c09 | ||
|
|
835bb49562 | ||
|
|
ab43d4995b | ||
|
|
f1e2ce3e09 | ||
|
|
6fa34aef04 | ||
|
|
d8748c9054 | ||
|
|
6f8a54273d | ||
|
|
37a7c70c2b | ||
|
|
7071e6f080 | ||
|
|
4c1a360797 | ||
|
|
f7ea7b165f | ||
|
|
29fcdcd766 | ||
|
|
62f91f746e | ||
|
|
d27aad6e67 | ||
|
|
3d55c27b10 | ||
|
|
30b18bef2b | ||
|
|
221b5d104c | ||
|
|
826ac1abe9 | ||
|
|
e1c327235a | ||
|
|
e6bf327b25 | ||
|
|
7e0bf72a89 | ||
|
|
1d27986fa1 | ||
|
|
4bfb958146 | ||
|
|
33e3e5e63d | ||
|
|
564f56e0f5 | ||
|
|
1a6a66406b | ||
|
|
84316fdfb5 | ||
|
|
a6eac0a01e | ||
|
|
412d858c49 | ||
|
|
7d7bb0591f | ||
|
|
9de6ec7279 | ||
|
|
ba94bff8d4 | ||
|
|
979cfb0ba8 | ||
|
|
f36821148f | ||
|
|
85fcc52b84 | ||
|
|
6b851cccdb | ||
|
|
540979f9fb | ||
|
|
2add5e2d25 | ||
|
|
8d58383c2e | ||
|
|
a3bb7af634 | ||
|
|
114016bbca | ||
|
|
8ba35d8810 | ||
|
|
73a01d56a7 | ||
|
|
afd8d8a020 | ||
|
|
90f0282ce3 | ||
|
|
468eff690d | ||
|
|
5e04480cc0 | ||
|
|
ea2b1519d5 | ||
|
|
e5385f7512 | ||
|
|
e13fe32f78 | ||
|
|
d8bf4b9f89 | ||
|
|
2d2c55e07c | ||
|
|
f6d38fe1ce | ||
|
|
b85cf2fdb6 | ||
|
|
d8df5f1ea1 | ||
|
|
10c587cbc2 | ||
|
|
e409524033 | ||
|
|
29d9c138c9 | ||
|
|
8304a8e0e4 | ||
|
|
64dc730a3b | ||
|
|
3096404db5 | ||
|
|
6818f0b1c3 | ||
|
|
3ddc073874 | ||
|
|
4f3468de7b | ||
|
|
7a9d7202e5 | ||
|
|
09bfab03a7 | ||
|
|
52cd8b7992 | ||
|
|
1cb7ea49ec | ||
|
|
9989820e17 | ||
|
|
798c605492 | ||
|
|
ffa4645d25 | ||
|
|
ca0656ce35 | ||
|
|
e4178d9ae6 | ||
|
|
843dfa5c6c | ||
|
|
52e2276445 | ||
|
|
9f78fc5341 | ||
|
|
bf0d49cc76 | ||
|
|
b5b2338c82 | ||
|
|
cef4d899e3 | ||
|
|
f6655343c7 | ||
|
|
4efababbb4 | ||
|
|
5a258b9da1 | ||
|
|
ef248bd5fb | ||
|
|
9c737d906b | ||
|
|
b8d2f7ece5 | ||
|
|
5990422a2d | ||
|
|
ec500939df | ||
|
|
fddaebf254 | ||
|
|
d6bc9f120e | ||
|
|
1705266779 | ||
|
|
97d64c3e01 | ||
|
|
f839cf79d7 | ||
|
|
844891c0ae | ||
|
|
c417206e7c | ||
|
|
5326e6571b | ||
|
|
681d930da3 | ||
|
|
f102419d7c | ||
|
|
a863ac773a | ||
|
|
26e92c6a8a | ||
|
|
9f5172c1e9 | ||
|
|
519ade1e51 | ||
|
|
356cd0287c | ||
|
|
e84a26be76 | ||
|
|
d6c8aeee2a | ||
|
|
8c3f11b3d5 | ||
|
|
e37a9ad059 | ||
|
|
9fc11ce475 | ||
|
|
9a32c59340 | ||
|
|
c78d19de0d | ||
|
|
1353f1f56c | ||
|
|
ee34d83cd8 | ||
|
|
d68449d25b | ||
|
|
d34c83a798 | ||
|
|
6e485bcd5a | ||
|
|
bab3a49b0b | ||
|
|
55796eca5c | ||
|
|
abf497d509 | ||
|
|
a76a5cda38 | ||
|
|
377a9d915e | ||
|
|
c7b3972fa0 | ||
|
|
fda53c0520 | ||
|
|
f2e789537c | ||
|
|
3287ef6a57 | ||
|
|
74b3d8f493 | ||
|
|
e0972f4636 | ||
|
|
42f914f6aa | ||
|
|
58f4fff864 | ||
|
|
681c9743b1 | ||
|
|
cd2d2a1238 | ||
|
|
905763e755 | ||
|
|
2550482432 | ||
|
|
8d028066ae | ||
|
|
bd4adad545 | ||
|
|
c40f9b1de0 | ||
|
|
8237250453 | ||
|
|
c8a7ed2022 | ||
|
|
4847c5a308 | ||
|
|
f6985bae41 | ||
|
|
cc9df4f5c5 | ||
|
|
83b7b4e599 | ||
|
|
fdc573009c | ||
|
|
51d769b0a1 | ||
|
|
f6b04d05b2 | ||
|
|
2926412604 | ||
|
|
ee21b1feda | ||
|
|
2d0140f705 | ||
|
|
183bd4143c | ||
|
|
57ac67a106 | ||
|
|
4b6d809a3b | ||
|
|
0221556ae7 | ||
|
|
6a1955d20d | ||
|
|
8c7d2c58f5 | ||
|
|
5b7746b464 | ||
|
|
e6e0cef91e | ||
|
|
bdc4c4a18d | ||
|
|
49efa02e09 | ||
|
|
fd96bcbd3f | ||
|
|
9647c3fa7d | ||
|
|
1e411bbef5 | ||
|
|
7c47a4db72 | ||
|
|
ae33104ecf | ||
|
|
b90a9e8cab | ||
|
|
4024cf0492 | ||
|
|
e07f9a7b49 | ||
|
|
61f778196b | ||
|
|
93897f86d0 | ||
|
|
095e08441f | ||
|
|
da63463a3c | ||
|
|
8e140bae6c | ||
|
|
3b2eb02f8a | ||
|
|
e53f5b4023 | ||
|
|
02cddb5c08 | ||
|
|
cb4d11a1d4 | ||
|
|
139535e83c | ||
|
|
b9e95f6d82 | ||
|
|
79cb668a50 | ||
|
|
8078516e2f | ||
|
|
1729bd23b2 | ||
|
|
ac7fc7ae69 | ||
|
|
4b570dc3f0 | ||
|
|
aa3d66df7b | ||
|
|
a16e05ed4d | ||
|
|
84dc5682d5 | ||
|
|
a112de41c5 | ||
|
|
02bba85fe7 | ||
|
|
56f68d33df | ||
|
|
d4edf9ed13 | ||
|
|
12b38e2a27 | ||
|
|
9f1807a128 | ||
|
|
6bdfcc3fb5 | ||
|
|
d8dd1918c4 | ||
|
|
376c5e47f5 | ||
|
|
edfb9e0bd7 | ||
|
|
eab4c7f28a | ||
|
|
c361340252 | ||
|
|
41e197003e | ||
|
|
c7ba37ec49 | ||
|
|
ba82aab563 | ||
|
|
f580dc9145 | ||
|
|
421dca2f99 | ||
|
|
991b2b1c3b | ||
|
|
d22743a1cf | ||
|
|
a2039c0b76 | ||
|
|
c549ddd149 | ||
|
|
f3721f712a | ||
|
|
480757dcfe | ||
|
|
fbaeff8920 | ||
|
|
de88ec4065 | ||
|
|
f149ba33d4 | ||
|
|
b15f2eed8f | ||
|
|
ec63dc22d4 | ||
|
|
7c7b98399c | ||
|
|
9d679e726e | ||
|
|
070ec03c0c | ||
|
|
be89224c36 | ||
|
|
8edd3c87cb | ||
|
|
9ac3cd73a8 | ||
|
|
203170c36d | ||
|
|
6f5bfdb726 | ||
|
|
b499857871 | ||
|
|
1388b0ca21 | ||
|
|
af095bdf4a | ||
|
|
7af361fa16 | ||
|
|
069f231b3a | ||
|
|
def028e970 | ||
|
|
16f49d13c8 | ||
|
|
e72d7979e0 | ||
|
|
ca4c7b5361 | ||
|
|
7ad6bb048c | ||
|
|
d2c9ac079a | ||
|
|
abe1257bd5 | ||
|
|
3db22fd2db | ||
|
|
a423067b36 | ||
|
|
33aab0357f | ||
|
|
34e9c69a62 | ||
|
|
fdf5ca0067 | ||
|
|
6eb17b847f | ||
|
|
918ebb8ecc | ||
|
|
c0ac52810e | ||
|
|
b658cddc71 | ||
|
|
fe753db78c | ||
|
|
2f1e47e35a | ||
|
|
c3ad337398 | ||
|
|
cc759868b8 | ||
|
|
1004435e9e | ||
|
|
a9e749a81f | ||
|
|
553fe15ba9 | ||
|
|
fec758d237 | ||
|
|
70a1d5d0f7 | ||
|
|
c66308e4e9 | ||
|
|
21aae468f2 | ||
|
|
f007b83ccc | ||
|
|
28be12d827 | ||
|
|
93bd6b39a4 | ||
|
|
75045ffd58 | ||
|
|
ed8419986a | ||
|
|
954011b2c4 | ||
|
|
c3f221f1fb | ||
|
|
de59f53de3 | ||
|
|
04b169fab8 | ||
|
|
857ec3b099 | ||
|
|
b1dd377b67 | ||
|
|
81128e8446 | ||
|
|
86371e2082 | ||
|
|
bd6f62dd68 | ||
|
|
d2275c05fb | ||
|
|
4988881568 | ||
|
|
f9be1ddfc5 | ||
|
|
60e95458a7 | ||
|
|
2bb6b0a2a9 | ||
|
|
3311bd27cb | ||
|
|
5d9ef0dcca | ||
|
|
7ddecdd9ef | ||
|
|
c8f966b0e5 | ||
|
|
3d2f21be7c | ||
|
|
3e876f5bd8 | ||
|
|
a5e56438c7 | ||
|
|
4e79259c4e | ||
|
|
fae79e5c3f | ||
|
|
b169be9dda | ||
|
|
8f4496346d | ||
|
|
f306c03030 | ||
|
|
a8469a9b92 | ||
|
|
b63510acb5 | ||
|
|
3e612ec88d | ||
|
|
1be36ae552 | ||
|
|
445b657207 | ||
|
|
da6dff2d17 | ||
|
|
b4aab6134e | ||
|
|
1c465a8cda | ||
|
|
8ab6c2d83b | ||
|
|
c89d075200 | ||
|
|
6c6c71484d | ||
|
|
18a66bae4e | ||
|
|
68b72f3162 | ||
|
|
5098973e0d | ||
|
|
d0976ff429 | ||
|
|
157783f782 | ||
|
|
e3a70373e1 | ||
|
|
a6e5563f8e | ||
|
|
470d84a843 | ||
|
|
80037f7e8d | ||
|
|
cc848c90a7 | ||
|
|
e0c293c5b4 | ||
|
|
2d82013261 | ||
|
|
5a6561f158 | ||
|
|
f28588ecb6 | ||
|
|
6579ea5b56 | ||
|
|
9373688be2 | ||
|
|
f6438065e3 | ||
|
|
2cec06be18 | ||
|
|
3a60bb306c | ||
|
|
e2ddb0d6d0 | ||
|
|
0049265c8a | ||
|
|
1f27867565 | ||
|
|
faf0cdec35 | ||
|
|
4c6961f7d6 | ||
|
|
0bd27c4182 | ||
|
|
ddcfac4f0f | ||
|
|
3e58014dbd | ||
|
|
ff06920e24 | ||
|
|
6073eab496 | ||
|
|
50ddc611a4 | ||
|
|
0ef2cfa753 | ||
|
|
790e503ad9 | ||
|
|
f6dbc0c882 | ||
|
|
a6c226aac9 | ||
|
|
2d41a3353d | ||
|
|
34e4208b3e | ||
|
|
d51486c62a | ||
|
|
f62f00d4d9 | ||
|
|
44e6ef3146 | ||
|
|
dea49d54c9 | ||
|
|
8a9523fa48 | ||
|
|
5dba0183c0 | ||
|
|
0108f86180 | ||
|
|
bc71d83697 | ||
|
|
0754c6263e | ||
|
|
2c037803f0 | ||
|
|
48410d0cf6 | ||
|
|
66b0f3c9df | ||
|
|
d8910eea73 | ||
|
|
f7f965a812 | ||
|
|
d806bdbca7 | ||
|
|
19b1196633 | ||
|
|
88f2b1ad50 | ||
|
|
f5ea19b15d | ||
|
|
d2645ecfb1 | ||
|
|
44b75d50f4 | ||
|
|
df23aacbc0 | ||
|
|
7d4dfe06e2 | ||
|
|
e50af4c981 | ||
|
|
4fdab6fd52 | ||
|
|
c54450f582 | ||
|
|
6bf9693146 | ||
|
|
422fd8b157 | ||
|
|
bf061b460b | ||
|
|
a035760b00 | ||
|
|
85c0454c49 | ||
|
|
7bfb0401c6 | ||
|
|
c097f4a450 | ||
|
|
a3faa108c5 | ||
|
|
c89b76ba80 | ||
|
|
80636e8a91 | ||
|
|
206845e106 | ||
|
|
64d015065f | ||
|
|
56af56e341 | ||
|
|
3086e1cbc1 | ||
|
|
b032a55045 | ||
|
|
d71c56b11f | ||
|
|
98ee242812 | ||
|
|
79267c8123 | ||
|
|
d2ad0e7c74 |
@@ -39,7 +39,7 @@ _APP_REDIS_HOST=redis
|
||||
_APP_REDIS_PORT=6379
|
||||
_APP_REDIS_PASS=
|
||||
_APP_REDIS_USER=
|
||||
COMPOSE_PROFILES=mongodb
|
||||
COMPOSE_PROFILES=mariadb,mongodb,postgresql
|
||||
_APP_DB_ADAPTER=mongodb
|
||||
_APP_DB_HOST=mongodb
|
||||
_APP_DB_PORT=27017
|
||||
@@ -47,6 +47,15 @@ _APP_DB_SCHEMA=appwrite
|
||||
_APP_DB_USER=user
|
||||
_APP_DB_PASS=password
|
||||
_APP_DB_ROOT_PASS=rootsecretpassword
|
||||
_APP_DB_ADAPTER_DOCUMENTSDB=mongodb
|
||||
_APP_DB_HOST_DOCUMENTSDB=mongodb
|
||||
_APP_DB_PORT_DOCUMENTSDB=27017
|
||||
_APP_DB_ADAPTER_VECTORSDB=postgresql
|
||||
_APP_DB_HOST_VECTORSDB=postgresql
|
||||
_APP_DB_PORT_VECTORSDB=5432
|
||||
_APP_EMBEDDING_MODELS=embeddinggemma
|
||||
_APP_EMBEDDING_ENDPOINT='http://ollama:11434/api/embed'
|
||||
_APP_EMBEDDING_TIMEOUT=30000
|
||||
_APP_STORAGE_DEVICE=Local
|
||||
_APP_STORAGE_S3_ACCESS_KEY=
|
||||
_APP_STORAGE_S3_SECRET=
|
||||
|
||||
+53
-37
@@ -7,7 +7,6 @@ concurrency:
|
||||
env:
|
||||
COMPOSE_FILE: docker-compose.yml
|
||||
IMAGE: appwrite-dev
|
||||
CACHE_KEY: appwrite-dev-${{ github.event.pull_request.head.sha }}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -151,8 +150,16 @@ 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
|
||||
|
||||
locale:
|
||||
name: Checks / Locale
|
||||
@@ -248,11 +255,12 @@ jobs:
|
||||
TESTING=true
|
||||
VERSION=dev
|
||||
|
||||
- name: Cache Docker Image
|
||||
uses: actions/cache@v5
|
||||
- name: Upload Docker Image
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
retention-days: 1
|
||||
|
||||
unit:
|
||||
name: Tests / Unit
|
||||
@@ -265,12 +273,11 @@ jobs:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v5
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
@@ -313,12 +320,11 @@ jobs:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v5
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
@@ -363,7 +369,7 @@ jobs:
|
||||
|
||||
e2e_service:
|
||||
name: Tests / E2E / ${{ matrix.database }} (${{ matrix.mode }}) / ${{ matrix.service }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.runner || 'ubuntu-latest' }}
|
||||
needs: [build, matrix]
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -396,18 +402,31 @@ jobs:
|
||||
Webhooks,
|
||||
VCS,
|
||||
Messaging,
|
||||
Migrations
|
||||
Migrations,
|
||||
Project
|
||||
]
|
||||
include:
|
||||
- service: Databases
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: Sites
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: Functions
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: Avatars
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: Realtime
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: TablesDB
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v5
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Set database environment
|
||||
run: |
|
||||
@@ -496,12 +515,11 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v5
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
@@ -555,12 +573,11 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v5
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
@@ -616,12 +633,11 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v5
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Build the Docker image
|
||||
run: DOCKER_BUILDKIT=1 docker build . --target production -t appwrite_image:latest
|
||||
- name: Run Trivy vulnerability scanner on image
|
||||
uses: aquasecurity/trivy-action@0.20.0
|
||||
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
|
||||
with:
|
||||
image-ref: 'appwrite_image:latest'
|
||||
format: 'sarif'
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
- name: Run Trivy vulnerability scanner on filesystem
|
||||
uses: aquasecurity/trivy-action@0.20.0
|
||||
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
|
||||
with:
|
||||
scan-type: 'fs'
|
||||
format: 'sarif'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,107 +1,120 @@
|
||||
# AGENTS.md
|
||||
# Appwrite
|
||||
|
||||
Appwrite is an end-to-end backend server for web, mobile, native, and backend apps. This guide provides context and instructions for AI coding agents working on the Appwrite codebase.
|
||||
Self-hosted Backend-as-a-Service platform. Hybrid monolithic-microservice architecture built with PHP 8.3+ on Swoole, delivered as Docker containers.
|
||||
|
||||
## Project Overview
|
||||
## Commands
|
||||
|
||||
Appwrite is a self-hosted Backend-as-a-Service (BaaS) platform that provides developers with a set of APIs and tools to build secure, scalable applications. The project uses a hybrid monolithic-microservice architecture built with PHP, running on Swoole for high performance.
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `docker compose up -d --force-recreate --build` | Build and start all services |
|
||||
| `docker compose exec appwrite test tests/e2e/Services/[Service]` | Run E2E tests for a service |
|
||||
| `docker compose exec appwrite test tests/e2e/Services/[Service] --filter=[Method]` | Run a single test method |
|
||||
| `docker compose exec appwrite test tests/unit/` | Run unit tests |
|
||||
| `composer format` | Auto-format code (Pint, PSR-12) |
|
||||
| `composer format <file>` | Format a specific file |
|
||||
| `composer lint <file>` | Check formatting of a file |
|
||||
| `composer analyze` | Static analysis (PHPStan level 3) |
|
||||
| `composer check` | Same as `analyze` |
|
||||
|
||||
**Key Technologies:**
|
||||
- **Backend:** PHP 8.3+, Swoole
|
||||
- **Libraries:** Utopia PHP
|
||||
- **Database:** MariaDB, Redis
|
||||
- **Cache:** Redis
|
||||
- **Queue:** Redis
|
||||
- **Containers:** Docker
|
||||
## Stack
|
||||
|
||||
## Development Commands
|
||||
- PHP 8.3+, Swoole 6.x (async runtime, replaces PHP-FPM)
|
||||
- Utopia PHP framework (HTTP routing, CLI, DI, queue)
|
||||
- MongoDB (default), MariaDB, MySQL, PostgreSQL (adapters via utopia-php/database)
|
||||
- Redis (cache, queue, pub/sub)
|
||||
- Docker + Traefik (reverse proxy)
|
||||
- PHPUnit 12, Pint (PSR-12), PHPStan level 3
|
||||
|
||||
```bash
|
||||
# Run Appwrite
|
||||
docker compose up -d --force-recreate --build
|
||||
## Project layout
|
||||
|
||||
# Run specific test
|
||||
docker compose exec appwrite test /usr/src/code/tests/e2e/Services/[ServiceName] --filter=[FunctionName]
|
||||
- **src/Appwrite/Platform/Modules/** -- feature modules (Account, Avatars, Compute, Console, Databases, Functions, Health, Project, Projects, Proxy, Sites, Storage, Teams, Tokens, VCS, Webhooks)
|
||||
- **src/Appwrite/Platform/Workers/** -- background job workers
|
||||
- **src/Appwrite/Platform/Tasks/** -- CLI tasks
|
||||
- **app/init.php** -- bootstrap (registers services, resources, listeners)
|
||||
- **app/init/** -- configs, constants, locales, models, registers, resources, span, database filters/formats
|
||||
- **bin/** -- CLI entry points: `worker-*` (14 workers), `schedule-*`, `queue-*`, plus `doctor`, `install`, `migrate`, `realtime`, `upgrade`, `ssl`, `vars`, `maintenance`, `interval`, `specs`, `sdks`, etc.
|
||||
- **tests/e2e/** -- end-to-end tests per service
|
||||
- **tests/unit/** -- unit tests
|
||||
- **public/** -- static assets and generated SDKs
|
||||
|
||||
# Format code
|
||||
composer format
|
||||
## Module structure
|
||||
|
||||
Each module under `src/Appwrite/Platform/Modules/{Name}/` contains:
|
||||
|
||||
```
|
||||
Module.php -- registers all services for the module
|
||||
Services/Http.php -- registers HTTP endpoints
|
||||
Services/Workers.php -- registers background workers
|
||||
Services/Tasks.php -- registers CLI tasks
|
||||
Http/{Service}/ -- endpoint actions (Create.php, Get.php, Update.php, Delete.php, XList.php)
|
||||
Workers/ -- worker implementations
|
||||
Tasks/ -- CLI task implementations
|
||||
```
|
||||
|
||||
## Code Style Guidelines
|
||||
HTTP endpoint nesting reflects the URL path. Sub-resources get subdirectories. For example, within the Functions module:
|
||||
`Http/Deployments/Template/Create.php` -> `POST /v1/functions/:functionId/deployments/template`
|
||||
|
||||
- Follow [PSR-12](https://www.php-fig.org/psr/psr-12/) coding standard
|
||||
- Use PSR-4 autoloading
|
||||
- Strict type declarations where applicable
|
||||
- Comprehensive PHPDoc comments
|
||||
File names in Http directories must only be `Get.php`, `Create.php`, `Update.php`, `Delete.php`, or `XList.php`. For non-CRUD operations, model the endpoint as a property update. For example, updating a team membership status lives at `Teams/Http/Memberships/Status/Update.php` (`PATCH /v1/teams/:teamId/memberships/:membershipId/status`).
|
||||
|
||||
### Naming Conventions
|
||||
Register new modules in `src/Appwrite/Platform/Appwrite.php`. Detailed module guide: `src/Appwrite/Platform/AGENTS.md`.
|
||||
|
||||
#### `resourceType` Naming Rule
|
||||
## Action pattern (HTTP endpoints)
|
||||
|
||||
When a collection has a combination of `resourceType`, `resourceId`, and/or `resourceInternalId`, the value of `resourceType` MUST always be **plural** - for example: `functions`, `sites`, `deployments`.
|
||||
|
||||
Examples:
|
||||
```php
|
||||
'resourceType' => 'functions'
|
||||
'resourceType' => 'sites'
|
||||
'resourceType' => 'deployments'
|
||||
class Create extends Action
|
||||
{
|
||||
public static function getName(): string { return 'createTeam'; }
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/teams')
|
||||
->desc('Create team')
|
||||
->groups(['api', 'teams'])
|
||||
->label('event', 'teams.[teamId].create')
|
||||
->label('scope', 'teams.write')
|
||||
->param('teamId', '', new CustomId(), 'Team ID.')
|
||||
->param('name', null, new Text(128), 'Team name.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $teamId,
|
||||
string $name,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Event $queueForEvents,
|
||||
): void {
|
||||
// implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Patterns
|
||||
Common injections: `$response`, `$request`, `$dbForProject`, `$dbForPlatform`, `$user`, `$project`, `$queueForEvents`, `$queueForMails`, `$queueForDeletes`.
|
||||
|
||||
### Document Update Optimization
|
||||
## Conventions
|
||||
|
||||
When updating documents, always pass only the changed attributes as a sparse `Document` rather than the full document. This is more efficient because `updateDocument()` internally performs `array_merge($old, $new)`.
|
||||
- PSR-12 formatting enforced by Pint. PSR-4 autoloading.
|
||||
- `resourceType` values are always **plural**: `'functions'`, `'sites'`, `'deployments'`.
|
||||
- When updating documents, pass only changed attributes as a sparse Document:
|
||||
```php
|
||||
// correct
|
||||
$dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'name' => $name,
|
||||
]));
|
||||
// incorrect -- passing full document is inefficient
|
||||
$user->setAttribute('name', $name);
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
```
|
||||
Exceptions: migrations, `array_merge()` with `getArrayCopy()`, updates where nearly all attributes change, complex nested relationship logic requiring full document state.
|
||||
- Avoid introducing dependencies outside the `utopia-php` ecosystem.
|
||||
- Never hardcode credentials -- use environment variables.
|
||||
- Code changes may require container restart. No central log location -- check relevant containers.
|
||||
|
||||
**Correct Pattern:**
|
||||
```php
|
||||
// Good: Pass only changed attributes directly
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
]));
|
||||
```
|
||||
## Cross-repo context
|
||||
|
||||
**Incorrect Pattern:**
|
||||
```php
|
||||
$user->setAttribute('name', $name);
|
||||
$user->setAttribute('email', $email);
|
||||
|
||||
// Bad: Passing full document is inefficient
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
```
|
||||
|
||||
**Exceptions:**
|
||||
- Migration files (need full document updates by design)
|
||||
- Cases already using `array_merge()` with `getArrayCopy()`
|
||||
- Updates where almost all attributes of the document change at once (sparse update provides little benefit compared to passing the full document)
|
||||
- Complex nested relationship logic where full document state is required
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Critical Security Practices
|
||||
|
||||
- **Never hardcode credentials** - Use environment variables
|
||||
- **Rate limiting** - Respect abuse prevention mechanisms
|
||||
|
||||
## Dependencies
|
||||
|
||||
Avoid introducing new dependencies other than utopia-php.
|
||||
|
||||
## Adding new endpoints
|
||||
|
||||
When adding new endpoints, make sure to use modules and follow its patterns. Find instruction in [Modules AGENTS.md](src/Appwrite/Platform/AGENTS.md) file.
|
||||
|
||||
## Pull Request Guidelines
|
||||
### Before Submitting
|
||||
|
||||
- Run `composer format`
|
||||
- Update documentation if adding features
|
||||
- Add/update tests for your changes
|
||||
- Check that Docker build succeeds
|
||||
`docs/specs/authentication.drawio.svg`
|
||||
|
||||
## Known Issues and Gotchas
|
||||
|
||||
- **Hot Reload:** Code changes require container restart in some cases
|
||||
- **Logging:** There is no central place for logs, so when debugging, ensure to check all possibly relevant containers
|
||||
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.
|
||||
|
||||
+119
-86
@@ -1,100 +1,133 @@
|
||||
# Version 1.8.1
|
||||
# Version 1.9.0
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Notable changes
|
||||
|
||||
* Add branch deployments support in [#10486](https://github.com/appwrite/appwrite/pull/10486)
|
||||
* Add TanStack Start sites support in [#10681](https://github.com/appwrite/appwrite/pull/10681)
|
||||
* Add Next.js standalone support in [#10747](https://github.com/appwrite/appwrite/pull/10747)
|
||||
* Add Resend integration in [#10690](https://github.com/appwrite/appwrite/pull/10690)
|
||||
* Add option to enable/disable image transformations per-bucket in [#10722](https://github.com/appwrite/appwrite/pull/10722)
|
||||
* Add operators support in [#10735](https://github.com/appwrite/appwrite/pull/10735) and [#10800](https://github.com/appwrite/appwrite/pull/10800)
|
||||
* Add function and sites stats in [#10786](https://github.com/appwrite/appwrite/pull/10786)
|
||||
* Add disable count feature in [#10668](https://github.com/appwrite/appwrite/pull/10668)
|
||||
* Add ElevenLabs site template in [#10782](https://github.com/appwrite/appwrite/pull/10782)
|
||||
* Add suggested environment variables in [#10795](https://github.com/appwrite/appwrite/pull/10795)
|
||||
* Update GeoDB database in [#10890](https://github.com/appwrite/appwrite/pull/10890)
|
||||
* Update Flutter default build runtime in [#10807](https://github.com/appwrite/appwrite/pull/10807)
|
||||
* Upgrade runtimes in [#10804](https://github.com/appwrite/appwrite/pull/10804)
|
||||
* Add PostgreSQL database adapter in [#9772](https://github.com/appwrite/appwrite/pull/9772) and [#11293](https://github.com/appwrite/appwrite/pull/11293)
|
||||
* Add MongoDB support in [#11312](https://github.com/appwrite/appwrite/pull/11312)
|
||||
* Add new webhooks API in [#11033](https://github.com/appwrite/appwrite/pull/11033) and [#11566](https://github.com/appwrite/appwrite/pull/11566)
|
||||
* Add schedules API endpoints in [#11331](https://github.com/appwrite/appwrite/pull/11331)
|
||||
* Add project labels in [#11056](https://github.com/appwrite/appwrite/pull/11056) and project status attribute in [#11291](https://github.com/appwrite/appwrite/pull/11291)
|
||||
* Add resource-based API key structure in [#11003](https://github.com/appwrite/appwrite/pull/11003) with custom ID support in [#11277](https://github.com/appwrite/appwrite/pull/11277) and list queries in [#11278](https://github.com/appwrite/appwrite/pull/11278)
|
||||
* Add string types (varchar, text, mediumtext, longtext) for attributes in [#11174](https://github.com/appwrite/appwrite/pull/11174)
|
||||
* Add encrypt parameter to string attribute types in [#11334](https://github.com/appwrite/appwrite/pull/11334)
|
||||
* Add int64 format support for integer attributes in [#11123](https://github.com/appwrite/appwrite/pull/11123)
|
||||
* Add collection and row storage size in [#11254](https://github.com/appwrite/appwrite/pull/11254) and [#11069](https://github.com/appwrite/appwrite/pull/11069)
|
||||
* Add totalSize on list responses in [#11102](https://github.com/appwrite/appwrite/pull/11102)
|
||||
* Add custom start command for sites and functions in [#10842](https://github.com/appwrite/appwrite/pull/10842)
|
||||
* Add separate build/runtime specifications in [#10849](https://github.com/appwrite/appwrite/pull/10849)
|
||||
* Add deployment retention for sites and functions in [#10959](https://github.com/appwrite/appwrite/pull/10959)
|
||||
* Add auto-delete old deployments in [#10959](https://github.com/appwrite/appwrite/pull/10959)
|
||||
* Add custom JWT duration in [#11009](https://github.com/appwrite/appwrite/pull/11009)
|
||||
* Add multiple application domains support in [#10911](https://github.com/appwrite/appwrite/pull/10911)
|
||||
* Add GraphQL introspection in [#11159](https://github.com/appwrite/appwrite/pull/11159)
|
||||
* Add realtime query subscriptions in [#11202](https://github.com/appwrite/appwrite/pull/11202) and [#11237](https://github.com/appwrite/appwrite/pull/11237)
|
||||
* Add realtime metrics for connections, messages, and bandwidth in [#11438](https://github.com/appwrite/appwrite/pull/11438) and [#11488](https://github.com/appwrite/appwrite/pull/11488)
|
||||
* Add messaging resource migration support in [#11495](https://github.com/appwrite/appwrite/pull/11495)
|
||||
* Add cached documents list in [#10832](https://github.com/appwrite/appwrite/pull/10832)
|
||||
* Add project queries support in [#10990](https://github.com/appwrite/appwrite/pull/10990)
|
||||
* Add batch document creation in [#10894](https://github.com/appwrite/appwrite/pull/10894)
|
||||
* Add async screenshots in [#11110](https://github.com/appwrite/appwrite/pull/11110)
|
||||
* Add new file parameters (encryption, compression) in [#11135](https://github.com/appwrite/appwrite/pull/11135)
|
||||
* Add new site templates in [#10031](https://github.com/appwrite/appwrite/pull/10031)
|
||||
* Add VCS repository authorized field in [#11421](https://github.com/appwrite/appwrite/pull/11421)
|
||||
* Add trusted console projects in [#11248](https://github.com/appwrite/appwrite/pull/11248)
|
||||
|
||||
### Refactoring
|
||||
|
||||
* Refactor to Utopia Platform modules architecture in [#11035](https://github.com/appwrite/appwrite/pull/11035), [#11049](https://github.com/appwrite/appwrite/pull/11049), [#11057](https://github.com/appwrite/appwrite/pull/11057), [#11103](https://github.com/appwrite/appwrite/pull/11103), [#11208](https://github.com/appwrite/appwrite/pull/11208), and [#11398](https://github.com/appwrite/appwrite/pull/11398)
|
||||
* Refactor auth to single instance in [#10872](https://github.com/appwrite/appwrite/pull/10872) and [#11130](https://github.com/appwrite/appwrite/pull/11130)
|
||||
* Refactor usage metrics to stateless publisher pattern in [#11449](https://github.com/appwrite/appwrite/pull/11449)
|
||||
* Refactor messaging and queue in [#10961](https://github.com/appwrite/appwrite/pull/10961)
|
||||
* Refactor functions schedule in [#10913](https://github.com/appwrite/appwrite/pull/10913)
|
||||
* Refactor make Bus dispatch synchronous in [#11449](https://github.com/appwrite/appwrite/pull/11449)
|
||||
* Remove proxy container in [#11039](https://github.com/appwrite/appwrite/pull/11039)
|
||||
|
||||
### Performance
|
||||
|
||||
* Optimize updateDocument() calls to use sparse documents in [#11465](https://github.com/appwrite/appwrite/pull/11465)
|
||||
* Optimize Dockerfile in [#10947](https://github.com/appwrite/appwrite/pull/10947)
|
||||
* Improve domain caching in [#11346](https://github.com/appwrite/appwrite/pull/11346)
|
||||
* Improve memory usage in [#11345](https://github.com/appwrite/appwrite/pull/11345)
|
||||
* Fix memory leak in [#11067](https://github.com/appwrite/appwrite/pull/11067) and [#11241](https://github.com/appwrite/appwrite/pull/11241)
|
||||
* Improve realtime performance in [#11251](https://github.com/appwrite/appwrite/pull/11251)
|
||||
* Enable SMTP keep-alive to reuse connections across mail jobs in [#11496](https://github.com/appwrite/appwrite/pull/11496)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix duplicate document error while creating file in [#10891](https://github.com/appwrite/appwrite/pull/10891)
|
||||
* Fix "Update external deployment (authorize)" throwing 500 error due to invalid query in [#10888](https://github.com/appwrite/appwrite/pull/10888)
|
||||
* Fix error setting user password in [#10889](https://github.com/appwrite/appwrite/pull/10889)
|
||||
* Fix error generating email MFA challenges in [#10884](https://github.com/appwrite/appwrite/pull/10884)
|
||||
* Fix file token expiry in [#10877](https://github.com/appwrite/appwrite/pull/10877)
|
||||
* Fix TanStack Nitro default in [#10860](https://github.com/appwrite/appwrite/pull/10860)
|
||||
* Fix TanStack builds in [#10767](https://github.com/appwrite/appwrite/pull/10767)
|
||||
* Fix nullable validation in [#10819](https://github.com/appwrite/appwrite/pull/10819) and [#10778](https://github.com/appwrite/appwrite/pull/10778)
|
||||
* Fix WebP library in [#10738](https://github.com/appwrite/appwrite/pull/10738)
|
||||
* Fix batch writes in [#10812](https://github.com/appwrite/appwrite/pull/10812)
|
||||
* Fix error handler error in [#10719](https://github.com/appwrite/appwrite/pull/10719)
|
||||
* Fix Next 16 compatibility in [#10713](https://github.com/appwrite/appwrite/pull/10713)
|
||||
* Fix stats usage memory leak in [#10683](https://github.com/appwrite/appwrite/pull/10683)
|
||||
* Fix author URL in template deployments in [#10535](https://github.com/appwrite/appwrite/pull/10535)
|
||||
* Fix VCS lock deletion in [#10691](https://github.com/appwrite/appwrite/pull/10691)
|
||||
* Fix blocked user/resource errors from 401 to 403 in [#11469](https://github.com/appwrite/appwrite/pull/11469)
|
||||
* Fix OAuth for custom domains in [#10967](https://github.com/appwrite/appwrite/pull/10967) and [#11269](https://github.com/appwrite/appwrite/pull/11269)
|
||||
* Fix OAuth redirect custom scheme in [#11292](https://github.com/appwrite/appwrite/pull/11292)
|
||||
* Fix OAuth verified emails in [#10986](https://github.com/appwrite/appwrite/pull/10986)
|
||||
* Fix MFA recovery code validation in [#10925](https://github.com/appwrite/appwrite/pull/10925)
|
||||
* Fix users allow updating phone number to empty in [#11521](https://github.com/appwrite/appwrite/pull/11521)
|
||||
* Fix users optional name error in [#11413](https://github.com/appwrite/appwrite/pull/11413)
|
||||
* Fix file permissions in [#11026](https://github.com/appwrite/appwrite/pull/11026)
|
||||
* Fix bulk insert webhook validation in [#11022](https://github.com/appwrite/appwrite/pull/11022)
|
||||
* Fix execution status update in [#11134](https://github.com/appwrite/appwrite/pull/11134)
|
||||
* Fix execution timeout status in [#11400](https://github.com/appwrite/appwrite/pull/11400)
|
||||
* Fix CORS wildcard in [#10956](https://github.com/appwrite/appwrite/pull/10956)
|
||||
* Fix preflight requests in [#10943](https://github.com/appwrite/appwrite/pull/10943)
|
||||
* Fix SMTP auth check in [#10939](https://github.com/appwrite/appwrite/pull/10939)
|
||||
* Fix scheduled executions trigger in [#10922](https://github.com/appwrite/appwrite/pull/10922)
|
||||
* Fix schedule executions bug in [#10916](https://github.com/appwrite/appwrite/pull/10916)
|
||||
* Fix deployment enum missing canceled value in [#11179](https://github.com/appwrite/appwrite/pull/11179)
|
||||
* Fix invalid chunk total in [#11270](https://github.com/appwrite/appwrite/pull/11270)
|
||||
* Fix sites domains in [#11240](https://github.com/appwrite/appwrite/pull/11240) and [#11355](https://github.com/appwrite/appwrite/pull/11355)
|
||||
* Fix rule domains in [#11355](https://github.com/appwrite/appwrite/pull/11355) and [#11276](https://github.com/appwrite/appwrite/pull/11276)
|
||||
* Fix rules deletion in [#11575](https://github.com/appwrite/appwrite/pull/11575)
|
||||
* Fix VCS template flow in [#11275](https://github.com/appwrite/appwrite/pull/11275)
|
||||
* Fix VCS comment empty in [#11490](https://github.com/appwrite/appwrite/pull/11490)
|
||||
* Fix DSN VCS error in [#11364](https://github.com/appwrite/appwrite/pull/11364)
|
||||
* Fix email URL params encoding in [#11369](https://github.com/appwrite/appwrite/pull/11369)
|
||||
* Fix missing email warning in [#11378](https://github.com/appwrite/appwrite/pull/11378)
|
||||
* Fix race condition in builds worker in [#11336](https://github.com/appwrite/appwrite/pull/11336)
|
||||
* Fix realtime regions in [#11414](https://github.com/appwrite/appwrite/pull/11414)
|
||||
* Fix realtime errors in [#11573](https://github.com/appwrite/appwrite/pull/11573)
|
||||
* Fix realtime TablesDB channels in [#11404](https://github.com/appwrite/appwrite/pull/11404) and [#11430](https://github.com/appwrite/appwrite/pull/11430)
|
||||
* Fix database shared table reconciliation in [#11578](https://github.com/appwrite/appwrite/pull/11578)
|
||||
* Fix PostgreSQL race condition in shared mode project creation in [#11536](https://github.com/appwrite/appwrite/pull/11536)
|
||||
* Fix compression enabled env in [#11171](https://github.com/appwrite/appwrite/pull/11171)
|
||||
* Fix deletes bug in [#10965](https://github.com/appwrite/appwrite/pull/10965)
|
||||
* Fix devkey scopes in [#10984](https://github.com/appwrite/appwrite/pull/10984)
|
||||
* Fix phone auth limit in [#11143](https://github.com/appwrite/appwrite/pull/11143)
|
||||
* Fix relationship document ID validation in [#11193](https://github.com/appwrite/appwrite/pull/11193)
|
||||
* Fix stale project overwrites OAuth in [#11461](https://github.com/appwrite/appwrite/pull/11461)
|
||||
* Fix storage health error swallowing in [#11492](https://github.com/appwrite/appwrite/pull/11492)
|
||||
* Fix Origin validator type error in [#11297](https://github.com/appwrite/appwrite/pull/11297)
|
||||
* Fix getScreenshot image format in [#11017](https://github.com/appwrite/appwrite/pull/11017)
|
||||
* Fix migration error handling in [#11457](https://github.com/appwrite/appwrite/pull/11457)
|
||||
* Fix deprecation warnings in [#11227](https://github.com/appwrite/appwrite/pull/11227)
|
||||
|
||||
### Installer
|
||||
|
||||
* New installer UI in [#11175](https://github.com/appwrite/appwrite/pull/11175) and [#11247](https://github.com/appwrite/appwrite/pull/11247)
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* Add CSV export functionality in [#10546](https://github.com/appwrite/appwrite/pull/10546), [#10750](https://github.com/appwrite/appwrite/pull/10750), [#10813](https://github.com/appwrite/appwrite/pull/10813), and [#10847](https://github.com/appwrite/appwrite/pull/10847)
|
||||
* Add JWT disposition in [#10867](https://github.com/appwrite/appwrite/pull/10867)
|
||||
* Add screenshots endpoint in [#10675](https://github.com/appwrite/appwrite/pull/10675)
|
||||
* Add screenshot endpoint stats in [#10706](https://github.com/appwrite/appwrite/pull/10706)
|
||||
* Add users attributes in [#10688](https://github.com/appwrite/appwrite/pull/10688)
|
||||
* Add max build duration environment variable in [#10674](https://github.com/appwrite/appwrite/pull/10674)
|
||||
* Add custom realtime logger in [#10871](https://github.com/appwrite/appwrite/pull/10871)
|
||||
* Add logs in [#10869](https://github.com/appwrite/appwrite/pull/10869)
|
||||
* Improve MFA docs endpoint order in [#10793](https://github.com/appwrite/appwrite/pull/10793)
|
||||
* Auth refactor in [#10758](https://github.com/appwrite/appwrite/pull/10758), [#10837](https://github.com/appwrite/appwrite/pull/10837), [#10682](https://github.com/appwrite/appwrite/pull/10682), and [#10667](https://github.com/appwrite/appwrite/pull/10667)
|
||||
* Bump assistant to 0.8.4 in [#10887](https://github.com/appwrite/appwrite/pull/10887)
|
||||
* Bump database to 3.1.5 in [#10766](https://github.com/appwrite/appwrite/pull/10766)
|
||||
* Bump Utopia DNS in [#10761](https://github.com/appwrite/appwrite/pull/10761)
|
||||
* Update domains to 0.8.3 in [#10658](https://github.com/appwrite/appwrite/pull/10658)
|
||||
* Update domains to 0.9.1 in [#10678](https://github.com/appwrite/appwrite/pull/10678)
|
||||
* Update Apple Swift to 13.3.0 in [#10679](https://github.com/appwrite/appwrite/pull/10679)
|
||||
* Update Apple Swift in [#10663](https://github.com/appwrite/appwrite/pull/10663)
|
||||
* Update CLI to 10.2.2 in [#10672](https://github.com/appwrite/appwrite/pull/10672)
|
||||
* Update to CLI 12.0.0 in [#10853](https://github.com/appwrite/appwrite/pull/10853)
|
||||
* Update docs examples to use Permission class in [#10707](https://github.com/appwrite/appwrite/pull/10707)
|
||||
* Update SDK examples docs in [#10855](https://github.com/appwrite/appwrite/pull/10855)
|
||||
* Release Python SDK in [#10762](https://github.com/appwrite/appwrite/pull/10762)
|
||||
* Release Flutter 20.3.2 in [#10838](https://github.com/appwrite/appwrite/pull/10838)
|
||||
* Release Flutter/Dart add screenshot examples in [#10811](https://github.com/appwrite/appwrite/pull/10811)
|
||||
* Release PHP CLI in [#10791](https://github.com/appwrite/appwrite/pull/10791)
|
||||
* Release SDKs in [#10817](https://github.com/appwrite/appwrite/pull/10817)
|
||||
* Update SDKs in [#10694](https://github.com/appwrite/appwrite/pull/10694), [#10729](https://github.com/appwrite/appwrite/pull/10729), and [#10744](https://github.com/appwrite/appwrite/pull/10744)
|
||||
* Update SDK generator in [#10743](https://github.com/appwrite/appwrite/pull/10743)
|
||||
* Update database in [#10664](https://github.com/appwrite/appwrite/pull/10664)
|
||||
* Update README file in [#10763](https://github.com/appwrite/appwrite/pull/10763)
|
||||
* SDK release documentation in [#10745](https://github.com/appwrite/appwrite/pull/10745)
|
||||
* SDK release runtime config in [#10765](https://github.com/appwrite/appwrite/pull/10765)
|
||||
* Sync specs in [#10789](https://github.com/appwrite/appwrite/pull/10789)
|
||||
* Sync 1.8.0 in [#10677](https://github.com/appwrite/appwrite/pull/10677)
|
||||
* Add workflow for issue triage in [#10718](https://github.com/appwrite/appwrite/pull/10718)
|
||||
* Add issue auto-labeler in [#10700](https://github.com/appwrite/appwrite/pull/10700)
|
||||
* Add AI moderator repo in [#10717](https://github.com/appwrite/appwrite/pull/10717)
|
||||
* Browser bump in [#10850](https://github.com/appwrite/appwrite/pull/10850)
|
||||
* Template type enum override in [#10848](https://github.com/appwrite/appwrite/pull/10848)
|
||||
* VCS reference type in [#10852](https://github.com/appwrite/appwrite/pull/10852)
|
||||
* Index scope description in [#10851](https://github.com/appwrite/appwrite/pull/10851)
|
||||
* Config for environment in [#10833](https://github.com/appwrite/appwrite/pull/10833)
|
||||
* Format instance in [#10830](https://github.com/appwrite/appwrite/pull/10830)
|
||||
* Replace sleep in webhooks service in [#10656](https://github.com/appwrite/appwrite/pull/10656)
|
||||
* Update email composer in [#10720](https://github.com/appwrite/appwrite/pull/10720)
|
||||
* Update facts on GitHub sites and functions in [#10593](https://github.com/appwrite/appwrite/pull/10593) and [#10771](https://github.com/appwrite/appwrite/pull/10771)
|
||||
* Fix wrong user type in [#10875](https://github.com/appwrite/appwrite/pull/10875)
|
||||
* Fix limit and offset computation in [#10880](https://github.com/appwrite/appwrite/pull/10880)
|
||||
* Fix enum examples in [#10828](https://github.com/appwrite/appwrite/pull/10828)
|
||||
* Fix response models multi-methods in [#10815](https://github.com/appwrite/appwrite/pull/10815)
|
||||
* Fix undefined variable in [#10654](https://github.com/appwrite/appwrite/pull/10654)
|
||||
* Fix undefined sequence in [#10652](https://github.com/appwrite/appwrite/pull/10652)
|
||||
* Fix description in [#10702](https://github.com/appwrite/appwrite/pull/10702)
|
||||
* Fix warning in builds worker in [#10705](https://github.com/appwrite/appwrite/pull/10705)
|
||||
* Fix sites create deployment docs in [#10566](https://github.com/appwrite/appwrite/pull/10566)
|
||||
* Fix test dependencies projects in [#10655](https://github.com/appwrite/appwrite/pull/10655)
|
||||
* Fix list sites test in [#10726](https://github.com/appwrite/appwrite/pull/10726)
|
||||
* Add audits upgrade in [#10953](https://github.com/appwrite/appwrite/pull/10953)
|
||||
* Add graceful workers shutdown in [#11104](https://github.com/appwrite/appwrite/pull/11104)
|
||||
* Add pool resilience in [#11139](https://github.com/appwrite/appwrite/pull/11139)
|
||||
* Add function queue job TTL in [#11226](https://github.com/appwrite/appwrite/pull/11226)
|
||||
* Add cleanup stale executions in [#11146](https://github.com/appwrite/appwrite/pull/11146)
|
||||
* Add success abuse reset in [#11085](https://github.com/appwrite/appwrite/pull/11085)
|
||||
* Add SMTP connection validation in [#11079](https://github.com/appwrite/appwrite/pull/11079)
|
||||
* Add allow custom email sender in [#10945](https://github.com/appwrite/appwrite/pull/10945)
|
||||
* Add array domains env support in [#11213](https://github.com/appwrite/appwrite/pull/11213)
|
||||
* Add file create after success hook in [#11054](https://github.com/appwrite/appwrite/pull/11054)
|
||||
* Add delete subscribers in [#11115](https://github.com/appwrite/appwrite/pull/11115)
|
||||
* Add cursor plugin in [#11371](https://github.com/appwrite/appwrite/pull/11371)
|
||||
* Add observability spans in [#11320](https://github.com/appwrite/appwrite/pull/11320), [#11306](https://github.com/appwrite/appwrite/pull/11306), and [#11228](https://github.com/appwrite/appwrite/pull/11228)
|
||||
* Upgrade PHPStan to v2 with full codebase coverage in [#11550](https://github.com/appwrite/appwrite/pull/11550)
|
||||
* Upgrade Traefik in [#11265](https://github.com/appwrite/appwrite/pull/11265)
|
||||
* Upgrade utopia-php/queue in [#11239](https://github.com/appwrite/appwrite/pull/11239)
|
||||
* Upgrade spomky-labs/otphp in [#11263](https://github.com/appwrite/appwrite/pull/11263)
|
||||
* Bump utopia-php/database to stable 5.3.15 in [#11573](https://github.com/appwrite/appwrite/pull/11573)
|
||||
* Bump utopia-php/migration to 1.6.3 in [#11443](https://github.com/appwrite/appwrite/pull/11443)
|
||||
* Consolidate CI workflows in [#11531](https://github.com/appwrite/appwrite/pull/11531) and [#11551](https://github.com/appwrite/appwrite/pull/11551)
|
||||
* Hide deprecated methods from docs in [#10933](https://github.com/appwrite/appwrite/pull/10933)
|
||||
* Deprecate project-level attributes in [#11203](https://github.com/appwrite/appwrite/pull/11203)
|
||||
|
||||
# Version 1.8.0
|
||||
|
||||
|
||||
+3
-3
@@ -72,7 +72,7 @@ docker run -it --rm \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
|
||||
--entrypoint="install" \
|
||||
appwrite/appwrite:1.8.1
|
||||
appwrite/appwrite:1.9.0
|
||||
```
|
||||
|
||||
### Windows
|
||||
@@ -84,7 +84,7 @@ docker run -it --rm ^
|
||||
--volume //var/run/docker.sock:/var/run/docker.sock ^
|
||||
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
|
||||
--entrypoint="install" ^
|
||||
appwrite/appwrite:1.8.1
|
||||
appwrite/appwrite:1.9.0
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
@@ -94,7 +94,7 @@ docker run -it --rm `
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock `
|
||||
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
|
||||
--entrypoint="install" `
|
||||
appwrite/appwrite:1.8.1
|
||||
appwrite/appwrite:1.9.0
|
||||
```
|
||||
|
||||
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
|
||||
|
||||
@@ -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/authentication)** - 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,10 +71,11 @@ 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" \
|
||||
appwrite/appwrite:1.8.1
|
||||
appwrite/appwrite:1.9.0
|
||||
```
|
||||
|
||||
### Windows
|
||||
@@ -84,20 +84,22 @@ 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" ^
|
||||
appwrite/appwrite:1.8.1
|
||||
appwrite/appwrite:1.9.0
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
|
||||
```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" `
|
||||
appwrite/appwrite:1.8.1
|
||||
appwrite/appwrite:1.9.0
|
||||
```
|
||||
|
||||
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.
|
||||
@@ -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)!
|
||||
|
||||
|
||||
+5
-1
@@ -329,17 +329,20 @@ $setResource('bus', function (Registry $register) use ($cli) {
|
||||
|
||||
$setResource('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 +351,4 @@ $cli->shutdown()->action(fn () => Timer::clearAll());
|
||||
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
|
||||
require_once __DIR__ . '/init/span.php';
|
||||
run($cli->run(...));
|
||||
Console::exit($exitCode);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
$common = include __DIR__ . '/collections/common.php';
|
||||
$projects = include __DIR__ . '/collections/projects.php';
|
||||
$databases = include __DIR__ . '/collections/databases.php';
|
||||
$vectorsdb = include __DIR__ . '/collections/vectorsdb.php';
|
||||
$platform = include __DIR__ . '/collections/platform.php';
|
||||
$logs = include __DIR__ . '/collections/logs.php';
|
||||
|
||||
@@ -26,6 +27,7 @@ unset($common['files']);
|
||||
$collections = [
|
||||
'buckets' => $buckets,
|
||||
'databases' => $databases,
|
||||
'vectorsdb' => $vectorsdb,
|
||||
'projects' => array_merge_recursive($projects, $common),
|
||||
'console' => array_merge_recursive($platform, $common),
|
||||
'logs' => $logs,
|
||||
|
||||
@@ -419,6 +419,17 @@ return [
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('impersonator'),
|
||||
'type' => Database::VAR_BOOLEAN,
|
||||
'signed' => true,
|
||||
'size' => 0,
|
||||
'format' => '',
|
||||
'filters' => [],
|
||||
'required' => false,
|
||||
'default' => false,
|
||||
'array' => false,
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
@@ -491,6 +502,13 @@ return [
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('impersonator'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => [ID::custom('impersonator')],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -61,6 +61,15 @@ return [
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('database'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'size' => 2000,
|
||||
'required' => false,
|
||||
'signed' => true,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
]
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
|
||||
return [
|
||||
'collections' => [
|
||||
'$collection' => ID::custom('databases'),
|
||||
'$id' => ID::custom('collections'),
|
||||
'name' => 'Collections',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => ID::custom('databaseInternalId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('databaseId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'signed' => true,
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'format' => '',
|
||||
'filters' => [],
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('name'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'size' => 256,
|
||||
'required' => true,
|
||||
'signed' => true,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('dimension'),
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'size' => 0,
|
||||
'required' => true,
|
||||
'signed' => false,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('enabled'),
|
||||
'type' => Database::VAR_BOOLEAN,
|
||||
'signed' => true,
|
||||
'size' => 0,
|
||||
'format' => '',
|
||||
'filters' => [],
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('documentSecurity'),
|
||||
'type' => Database::VAR_BOOLEAN,
|
||||
'signed' => true,
|
||||
'size' => 0,
|
||||
'format' => '',
|
||||
'filters' => [],
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('attributes'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'size' => 1000000,
|
||||
'required' => false,
|
||||
'signed' => true,
|
||||
'array' => false,
|
||||
'filters' => ['subQueryAttributes'],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('indexes'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'size' => 1000000,
|
||||
'required' => false,
|
||||
'signed' => true,
|
||||
'array' => false,
|
||||
'filters' => ['subQueryIndexes'],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('search'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16384,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'defaultAttributes' => [
|
||||
[
|
||||
'$id' => ID::custom('embeddings'),
|
||||
'type' => Database::VAR_VECTOR,
|
||||
'required' => true,
|
||||
'signed' => false,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('metadata'),
|
||||
'type' => Database::VAR_OBJECT,
|
||||
'default' => [],
|
||||
'required' => false,
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => ID::custom('_fulltext_search'),
|
||||
'type' => Database::INDEX_FULLTEXT,
|
||||
'attributes' => ['search'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_name'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['name'],
|
||||
'lengths' => [256],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_enabled'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['enabled'],
|
||||
'lengths' => [],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_documentSecurity'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['documentSecurity'],
|
||||
'lengths' => [],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
],
|
||||
'defaultIndexes' => [
|
||||
// not creating default indexes on the embeddings as it depends on the type of query users using the most
|
||||
[
|
||||
'$id' => ID::custom('_key_metadata'),
|
||||
'type' => Database::INDEX_OBJECT,
|
||||
'attributes' => ['metadata'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
]
|
||||
]
|
||||
];
|
||||
@@ -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)) : [],
|
||||
|
||||
@@ -28,6 +28,9 @@ return [
|
||||
'X-Appwrite-Timestamp',
|
||||
'X-Appwrite-Session',
|
||||
'X-Appwrite-Platform',
|
||||
'X-Appwrite-Impersonate-User-Id',
|
||||
'X-Appwrite-Impersonate-User-Email',
|
||||
'X-Appwrite-Impersonate-User-Phone',
|
||||
// SDK generator
|
||||
'X-SDK-Version',
|
||||
'X-SDK-Name',
|
||||
|
||||
@@ -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.',
|
||||
@@ -1206,6 +1221,11 @@ return [
|
||||
'description' => 'Migration is already in progress. You can check the status of the migration in your Appwrite Console\'s "Settings" > "Migrations".',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::MIGRATION_DATABASE_TYPE_UNSUPPORTED => [
|
||||
'name' => Exception::MIGRATION_DATABASE_TYPE_UNSUPPORTED,
|
||||
'description' => 'The specified database type is not supported for CSV import or export operations.',
|
||||
'code' => 400,
|
||||
],
|
||||
|
||||
/** Realtime */
|
||||
Exception::REALTIME_MESSAGE_FORMAT_INVALID => [
|
||||
|
||||
@@ -57,21 +57,21 @@
|
||||
"emails.recovery.thanks": "Thanks,",
|
||||
"emails.recovery.buttonText": "Reset password",
|
||||
"emails.recovery.signature": "{{project}} team",
|
||||
"emails.csvExport.success.subject": "Your CSV export is ready",
|
||||
"emails.csvExport.success.preview": "Your data export has been completed successfully.",
|
||||
"emails.csvExport.success.hello": "Hello {{user}},",
|
||||
"emails.csvExport.success.body": "Your CSV export is ready to download. Click the button below to download your data export.",
|
||||
"emails.csvExport.success.footer": "This download link will expire in 1 hour.",
|
||||
"emails.csvExport.success.thanks": "Thanks,",
|
||||
"emails.csvExport.success.buttonText": "Download CSV",
|
||||
"emails.csvExport.success.signature": "Appwrite team",
|
||||
"emails.csvExport.failure.subject": "Your CSV export failed - file too large",
|
||||
"emails.csvExport.failure.preview": "Your data export failed because the file size exceeds your plan limit.",
|
||||
"emails.csvExport.failure.hello": "Hello {{user}},",
|
||||
"emails.csvExport.failure.body": "Your CSV export could not be completed because the export file size ({{size}}MB) exceeds your plan limit. Please consider upgrading your plan or exporting a smaller dataset.",
|
||||
"emails.csvExport.failure.footer": "If you have any questions, please contact our support team.",
|
||||
"emails.csvExport.failure.thanks": "Thanks,",
|
||||
"emails.csvExport.failure.signature": "{{project}} team",
|
||||
"emails.dataExport.success.subject": "Your {{type}} export is ready",
|
||||
"emails.dataExport.success.preview": "Your data export has been completed successfully.",
|
||||
"emails.dataExport.success.hello": "Hello {{user}},",
|
||||
"emails.dataExport.success.body": "Your {{type}} export is ready to download. Click the button below to download your data export.",
|
||||
"emails.dataExport.success.footer": "This download link will expire in 1 hour.",
|
||||
"emails.dataExport.success.thanks": "Thanks,",
|
||||
"emails.dataExport.success.buttonText": "Download {{type}}",
|
||||
"emails.dataExport.success.signature": "Appwrite team",
|
||||
"emails.dataExport.failure.subject": "Your {{type}} export failed - file too large",
|
||||
"emails.dataExport.failure.preview": "Your data export failed because the file size exceeds your plan limit.",
|
||||
"emails.dataExport.failure.hello": "Hello {{user}},",
|
||||
"emails.dataExport.failure.body": "Your {{type}} export could not be completed because the export file size ({{size}}MB) exceeds your plan limit. Please consider upgrading your plan or exporting a smaller dataset.",
|
||||
"emails.dataExport.failure.footer": "If you have any questions, please contact our support team.",
|
||||
"emails.dataExport.failure.thanks": "Thanks,",
|
||||
"emails.dataExport.failure.signature": "{{project}} team",
|
||||
"emails.invitation.subject": "Invitation to {{team}} Team at {{project}}",
|
||||
"emails.invitation.preview": "{{owner}} invited you to join {{team}} at {{project}}",
|
||||
"emails.invitation.hello": "Hello {{user}},",
|
||||
|
||||
@@ -62,6 +62,8 @@ $admins = [
|
||||
'devKeys.write',
|
||||
'webhooks.read',
|
||||
'webhooks.write',
|
||||
'project.read',
|
||||
'project.write',
|
||||
'locale.read',
|
||||
'avatars.read',
|
||||
'health.read',
|
||||
|
||||
@@ -31,12 +31,4 @@ return [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s development keys",
|
||||
],
|
||||
"webhooks.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s webhooks",
|
||||
],
|
||||
"webhooks.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s webhooks",
|
||||
],
|
||||
];
|
||||
|
||||
@@ -180,4 +180,12 @@ return [ // List of publicly visible scopes
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s webhooks",
|
||||
],
|
||||
"project.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s information",
|
||||
],
|
||||
"project.write" => [
|
||||
"description" =>
|
||||
"Access to update project\'s information",
|
||||
],
|
||||
];
|
||||
|
||||
+35
-24
@@ -250,26 +250,16 @@ return [
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'markdown',
|
||||
'name' => 'Markdown',
|
||||
'version' => '0.3.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-md.git',
|
||||
'package' => 'https://www.npmjs.com/package/@appwrite.io/docs',
|
||||
'enabled' => true,
|
||||
'beta' => false,
|
||||
'dev' => false,
|
||||
'hidden' => false,
|
||||
'family' => APP_SDK_PLATFORM_CONSOLE,
|
||||
'prism' => 'markdown',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/console-md'),
|
||||
'gitUrl' => 'git@github.com:appwrite/sdk-for-md.git',
|
||||
'gitRepoName' => 'sdk-for-md',
|
||||
'gitUserName' => 'appwrite',
|
||||
'gitBranch' => 'dev',
|
||||
'repoBranch' => 'main',
|
||||
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/md/CHANGELOG.md'),
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
APP_SDK_PLATFORM_STATIC => [
|
||||
'key' => APP_SDK_PLATFORM_STATIC,
|
||||
'name' => 'Static',
|
||||
'description' => 'SDK artifacts for Appwrite integrations that do not require a generated platform API specification.',
|
||||
'enabled' => true,
|
||||
'beta' => false,
|
||||
'sdks' => [
|
||||
[
|
||||
'key' => 'agent-skills',
|
||||
'name' => 'AgentSkills',
|
||||
@@ -279,9 +269,10 @@ return [
|
||||
'beta' => false,
|
||||
'dev' => false,
|
||||
'hidden' => false,
|
||||
'family' => APP_SDK_PLATFORM_CONSOLE,
|
||||
'spec' => 'static',
|
||||
'family' => APP_SDK_PLATFORM_STATIC,
|
||||
'prism' => 'agent-skills',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/console-agent-skills'),
|
||||
'source' => \realpath(__DIR__ . '/../sdks/static-agent-skills'),
|
||||
'gitUrl' => 'git@github.com:appwrite/agent-skills.git',
|
||||
'gitRepoName' => 'agent-skills',
|
||||
'gitUserName' => 'appwrite',
|
||||
@@ -298,9 +289,10 @@ return [
|
||||
'beta' => false,
|
||||
'dev' => false,
|
||||
'hidden' => false,
|
||||
'family' => APP_SDK_PLATFORM_CONSOLE,
|
||||
'spec' => 'static',
|
||||
'family' => APP_SDK_PLATFORM_STATIC,
|
||||
'prism' => 'cursor-plugin',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/console-cursor-plugin'),
|
||||
'source' => \realpath(__DIR__ . '/../sdks/static-cursor-plugin'),
|
||||
'gitUrl' => 'git@github.com:appwrite/cursor-plugin.git',
|
||||
'gitRepoName' => 'cursor-plugin',
|
||||
'gitUserName' => 'appwrite',
|
||||
@@ -494,6 +486,25 @@ return [
|
||||
'gitBranch' => 'dev',
|
||||
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/swift/CHANGELOG.md'),
|
||||
],
|
||||
[
|
||||
'key' => 'rust',
|
||||
'name' => 'Rust',
|
||||
'version' => '0.1.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-rust',
|
||||
'package' => 'https://crates.io/crates/appwrite',
|
||||
'enabled' => true,
|
||||
'beta' => true,
|
||||
'dev' => true,
|
||||
'hidden' => false,
|
||||
'family' => APP_SDK_PLATFORM_SERVER,
|
||||
'prism' => 'rust',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/server-rust'),
|
||||
'gitUrl' => 'git@github.com:appwrite/sdk-for-rust.git',
|
||||
'gitRepoName' => 'sdk-for-rust',
|
||||
'gitUserName' => 'appwrite',
|
||||
'gitBranch' => 'dev',
|
||||
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/rust/CHANGELOG.md'),
|
||||
],
|
||||
[
|
||||
'key' => 'graphql',
|
||||
'name' => 'GraphQL',
|
||||
|
||||
@@ -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',
|
||||
|
||||
+273
-69
@@ -16,7 +16,6 @@ use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Hooks\Hooks;
|
||||
use Appwrite\Network\Validator\Email as EmailValidator;
|
||||
use Appwrite\Network\Validator\Redirect;
|
||||
use Appwrite\OpenSSL\OpenSSL;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -60,6 +59,7 @@ use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Emails\Email;
|
||||
use Utopia\Emails\Validator\Email as EmailValidator;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\Storage\Validator\FileName;
|
||||
@@ -209,6 +209,22 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr
|
||||
|
||||
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, Authorization $authorization) {
|
||||
|
||||
// Attempt to decode secret as a JWT (used by OAuth2 token flow to carry provider info)
|
||||
$oauthProvider = null;
|
||||
try {
|
||||
$jwtDecoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 60, 0);
|
||||
$payload = $jwtDecoder->decode($secret);
|
||||
|
||||
if (empty($payload['provider'])) {
|
||||
throw new Exception(Exception::USER_INVALID_TOKEN);
|
||||
}
|
||||
|
||||
$oauthProvider = $payload['provider'];
|
||||
$secret = $payload['secret'];
|
||||
} catch (\Ahc\Jwt\JWTException) {
|
||||
// Not a JWT — use secret as-is (non-OAuth flows)
|
||||
}
|
||||
|
||||
/** @var Appwrite\Utopia\Database\Documents\User $userFromRequest */
|
||||
$userFromRequest = $authorization->skip(fn () => $dbForProject->getDocument('users', $userId));
|
||||
|
||||
@@ -220,6 +236,12 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||
?: $userFromRequest->tokenVerify(null, $secret, $proofForCode);
|
||||
|
||||
if (!$verifiedToken) {
|
||||
// Could mean invalid/expired JWT, or expired secret
|
||||
throw new Exception(Exception::USER_INVALID_TOKEN);
|
||||
}
|
||||
|
||||
// OAuth2 tokens must have a provider from the JWT
|
||||
if ($verifiedToken->getAttribute('type') === TOKEN_TYPE_OAUTH2 && $oauthProvider === null) {
|
||||
throw new Exception(Exception::USER_INVALID_TOKEN);
|
||||
}
|
||||
|
||||
@@ -245,7 +267,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||
TOKEN_TYPE_INVITE => SESSION_PROVIDER_EMAIL,
|
||||
TOKEN_TYPE_MAGIC_URL => SESSION_PROVIDER_MAGIC_URL,
|
||||
TOKEN_TYPE_PHONE => SESSION_PROVIDER_PHONE,
|
||||
TOKEN_TYPE_OAUTH2 => SESSION_PROVIDER_OAUTH2,
|
||||
TOKEN_TYPE_OAUTH2 => $oauthProvider,
|
||||
default => SESSION_PROVIDER_TOKEN,
|
||||
};
|
||||
$session = new Document(array_merge(
|
||||
@@ -381,7 +403,8 @@ Http::post('/v1/account')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Authorization $authorization, Hooks $hooks) {
|
||||
->inject('plan')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Authorization $authorization, Hooks $hooks, array $plan) {
|
||||
|
||||
$email = \strtolower($email);
|
||||
if ('console' === $project->getId()) {
|
||||
@@ -430,11 +453,38 @@ Http::post('/v1/account')
|
||||
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||
$proof = new ProofsPassword();
|
||||
$hash = $proof->hash($password);
|
||||
$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) {
|
||||
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -465,11 +515,11 @@ Http::post('/v1/account')
|
||||
'authenticators' => null,
|
||||
'search' => implode(' ', [$userId, $email, $name]),
|
||||
'accessedAt' => DateTime::now(),
|
||||
'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'],
|
||||
]);
|
||||
|
||||
$user->removeAttribute('$sequence');
|
||||
@@ -673,6 +723,7 @@ Http::delete('/v1/account/sessions')
|
||||
|
||||
$protocol = $request->getProtocol();
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
$currentSession = null;
|
||||
|
||||
foreach ($sessions as $session) {/** @var Document $session */
|
||||
$dbForProject->deleteDocument('sessions', $session->getId());
|
||||
@@ -694,6 +745,7 @@ Http::delete('/v1/account/sessions')
|
||||
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
|
||||
// Use current session for events.
|
||||
$currentSession = $session;
|
||||
$queueForEvents
|
||||
->setPayload($response->output($session, Response::MODEL_SESSION));
|
||||
|
||||
@@ -706,9 +758,11 @@ Http::delete('/v1/account/sessions')
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
->setParam('sessionId', $session->getId());
|
||||
if ($currentSession instanceof Document) {
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
->setParam('sessionId', $currentSession->getId());
|
||||
}
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
@@ -754,7 +808,8 @@ Http::get('/v1/account/sessions/:sessionId')
|
||||
->setAttribute('secret', $session->getAttribute('secret', ''))
|
||||
;
|
||||
|
||||
return $response->dynamic($session, Response::MODEL_SESSION);
|
||||
$response->dynamic($session, Response::MODEL_SESSION);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -934,7 +989,7 @@ Http::patch('/v1/account/sessions/:sessionId')
|
||||
->setPayload($response->output($session, Response::MODEL_SESSION))
|
||||
;
|
||||
|
||||
return $response->dynamic($session, Response::MODEL_SESSION);
|
||||
$response->dynamic($session, Response::MODEL_SESSION);
|
||||
});
|
||||
|
||||
Http::post('/v1/account/sessions/email')
|
||||
@@ -1482,8 +1537,9 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->inject('store')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) use ($oauthDefaultSuccess) {
|
||||
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, Authorization $authorization) use ($oauthDefaultSuccess) {
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$port = $request->getPort();
|
||||
$callbackBase = $protocol . '://' . $request->getHostname();
|
||||
@@ -1725,10 +1781,38 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
}
|
||||
}
|
||||
|
||||
$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) {
|
||||
$failureRedirect(Exception::GENERAL_INVALID_EMAIL);
|
||||
}
|
||||
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
|
||||
$failureRedirect(Exception::USER_EMAIL_DISPOSABLE);
|
||||
}
|
||||
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
|
||||
$failureRedirect(Exception::USER_EMAIL_NOT_CANONICAL);
|
||||
}
|
||||
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
|
||||
$failureRedirect(Exception::USER_EMAIL_FREE);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1758,11 +1842,11 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
'authenticators' => null,
|
||||
'search' => implode(' ', [$userId, $email, $name]),
|
||||
'accessedAt' => DateTime::now(),
|
||||
'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'],
|
||||
]);
|
||||
|
||||
$user->removeAttribute('$sequence');
|
||||
@@ -1837,19 +1921,47 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
}
|
||||
|
||||
if (empty($user->getAttribute('email'))) {
|
||||
$user->setAttribute('email', $oauth2->getUserEmail($accessToken));
|
||||
$email = $oauth2->getUserEmail($accessToken);
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => null,
|
||||
'emailIsCanonical' => null,
|
||||
'emailIsCorporate' => null,
|
||||
'emailIsDisposable' => null,
|
||||
'emailIsFree' => null,
|
||||
];
|
||||
|
||||
try {
|
||||
$emailCanonical = new Email($user->getAttribute('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) {
|
||||
$failureRedirect(Exception::GENERAL_INVALID_EMAIL);
|
||||
}
|
||||
|
||||
$user->setAttribute('emailCanonical', $emailCanonical?->getCanonical());
|
||||
$user->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported());
|
||||
$user->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate());
|
||||
$user->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable());
|
||||
$user->setAttribute('emailIsFree', $emailCanonical?->isFree());
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
|
||||
$failureRedirect(Exception::USER_EMAIL_DISPOSABLE);
|
||||
}
|
||||
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
|
||||
$failureRedirect(Exception::USER_EMAIL_NOT_CANONICAL);
|
||||
}
|
||||
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
|
||||
$failureRedirect(Exception::USER_EMAIL_FREE);
|
||||
}
|
||||
|
||||
$user->setAttribute('email', $email);
|
||||
$user->setAttribute('emailCanonical', $emailMetadata['emailCanonical']);
|
||||
$user->setAttribute('emailIsCanonical', $emailMetadata['emailIsCanonical']);
|
||||
$user->setAttribute('emailIsCorporate', $emailMetadata['emailIsCorporate']);
|
||||
$user->setAttribute('emailIsDisposable', $emailMetadata['emailIsDisposable']);
|
||||
$user->setAttribute('emailIsFree', $emailMetadata['emailIsFree']);
|
||||
}
|
||||
|
||||
if (empty($user->getAttribute('name'))) {
|
||||
@@ -1899,7 +2011,12 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->setParam('tokenId', $token->getId())
|
||||
;
|
||||
|
||||
$query['secret'] = $secret;
|
||||
// Wrap secret in a JWT that also carries the provider name
|
||||
$jwtEncoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 60, 0);
|
||||
$query['secret'] = $jwtEncoder->encode([
|
||||
'secret' => $secret,
|
||||
'provider' => $provider,
|
||||
]);
|
||||
$query['userId'] = $user->getId();
|
||||
|
||||
// If the `token` param is not set, we persist the session in a cookie
|
||||
@@ -1961,7 +2078,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
}
|
||||
|
||||
if (isset($sessionUpgrade) && $sessionUpgrade) {
|
||||
if (isset($sessionUpgrade) && $sessionUpgrade && isset($session)) {
|
||||
foreach ($user->getAttribute('targets', []) as $target) {
|
||||
if ($target->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) {
|
||||
continue;
|
||||
@@ -2122,10 +2239,11 @@ Http::post('/v1/account/tokens/magic-url')
|
||||
->inject('locale')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMails')
|
||||
->inject('plan')
|
||||
->inject('proofForPassword')
|
||||
->inject('platform')
|
||||
->inject('authorization')
|
||||
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) {
|
||||
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, array $plan, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) {
|
||||
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
|
||||
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
|
||||
}
|
||||
@@ -2160,10 +2278,38 @@ Http::post('/v1/account/tokens/magic-url')
|
||||
|
||||
$userId = $userId === 'unique()' ? ID::unique() : $userId;
|
||||
|
||||
$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) {
|
||||
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
|
||||
}
|
||||
|
||||
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->setAttributes([
|
||||
@@ -2190,11 +2336,11 @@ Http::post('/v1/account/tokens/magic-url')
|
||||
'authenticators' => null,
|
||||
'search' => implode(' ', [$userId, $email]),
|
||||
'accessedAt' => DateTime::now(),
|
||||
'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'],
|
||||
]);
|
||||
|
||||
$user->removeAttribute('$sequence');
|
||||
@@ -2402,10 +2548,11 @@ Http::post('/v1/account/tokens/email')
|
||||
->inject('locale')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMails')
|
||||
->inject('plan')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForCode')
|
||||
->inject('authorization')
|
||||
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) {
|
||||
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, array $plan, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) {
|
||||
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
|
||||
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
|
||||
}
|
||||
@@ -2438,10 +2585,38 @@ Http::post('/v1/account/tokens/email')
|
||||
|
||||
$userId = $userId === 'unique()' ? ID::unique() : $userId;
|
||||
|
||||
$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) {
|
||||
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
|
||||
}
|
||||
|
||||
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->setAttributes([
|
||||
@@ -2466,11 +2641,11 @@ Http::post('/v1/account/tokens/email')
|
||||
'memberships' => null,
|
||||
'search' => implode(' ', [$userId, $email]),
|
||||
'accessedAt' => DateTime::now(),
|
||||
'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'],
|
||||
]);
|
||||
|
||||
$user->removeAttribute('$sequence');
|
||||
@@ -3287,9 +3462,10 @@ Http::patch('/v1/account/email')
|
||||
->inject('queueForEvents')
|
||||
->inject('project')
|
||||
->inject('hooks')
|
||||
->inject('plan')
|
||||
->inject('proofForPassword')
|
||||
->inject('authorization')
|
||||
->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, ProofsPassword $proofForPassword, Authorization $authorization) {
|
||||
->inject('authorization')
|
||||
->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, array $plan, ProofsPassword $proofForPassword, Authorization $authorization) {
|
||||
// passwordUpdate will be empty if the user has never set a password
|
||||
$passwordUpdate = $user->getAttribute('passwordUpdate');
|
||||
|
||||
@@ -3317,20 +3493,48 @@ Http::patch('/v1/account/email')
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
|
||||
}
|
||||
|
||||
$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) {
|
||||
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
|
||||
}
|
||||
|
||||
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) // After this user needs to confirm mail again
|
||||
->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'])
|
||||
;
|
||||
|
||||
if (empty($passwordUpdate)) {
|
||||
@@ -3733,7 +3937,7 @@ Http::post('/v1/account/recovery')
|
||||
->setParam('userId', $profile->getId())
|
||||
->setParam('tokenId', $recovery->getId())
|
||||
->setUser($profile)
|
||||
->setPayload(Response::showSensitive(fn () => $response->output($recovery, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
->setPayload($response->showSensitive(fn () => $response->output($recovery, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -3834,7 +4038,7 @@ Http::put('/v1/account/recovery')
|
||||
$queueForEvents
|
||||
->setParam('userId', $profile->getId())
|
||||
->setParam('tokenId', $recoveryDocument->getId())
|
||||
->setPayload(Response::showSensitive(fn () => $response->output($recoveryDocument, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
->setPayload($response->showSensitive(fn () => $response->output($recoveryDocument, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
|
||||
$response->dynamic($recoveryDocument, Response::MODEL_TOKEN);
|
||||
});
|
||||
@@ -4064,7 +4268,7 @@ Http::post('/v1/account/verifications/email')
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
->setParam('tokenId', $verification->getId())
|
||||
->setPayload(Response::showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
->setPayload($response->showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -4156,7 +4360,7 @@ Http::put('/v1/account/verifications/email')
|
||||
$queueForEvents
|
||||
->setParam('userId', $userId)
|
||||
->setParam('tokenId', $verification->getId())
|
||||
->setPayload(Response::showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
->setPayload($response->showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
|
||||
$response->dynamic($verification, Response::MODEL_TOKEN);
|
||||
});
|
||||
@@ -4688,5 +4892,5 @@ Http::delete('/v1/account/identities/:identityId')
|
||||
->setParam('identityId', $identity->getId())
|
||||
->setPayload($response->output($identity, Response::MODEL_IDENTITY));
|
||||
|
||||
return $response->noContent();
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
@@ -28,12 +28,18 @@ use Utopia\Validator\Text;
|
||||
Http::init()
|
||||
->groups(['graphql'])
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('authorization')
|
||||
->action(function (Document $project, Authorization $authorization) {
|
||||
->action(function (Document $project, User $user, Request $request, Response $response, Authorization $authorization) {
|
||||
$response->setUser($user);
|
||||
$request->setUser($user);
|
||||
|
||||
if (
|
||||
array_key_exists('graphql', $project->getAttribute('apis', []))
|
||||
&& !$project->getAttribute('apis', [])['graphql']
|
||||
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
&& !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_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([
|
||||
|
||||
@@ -8,7 +8,6 @@ use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Messaging\Status as MessageStatus;
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Appwrite\Permission;
|
||||
use Appwrite\Role;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -43,6 +42,7 @@ use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\Roles;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\System\System;
|
||||
@@ -3566,7 +3566,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 {
|
||||
|
||||
@@ -29,6 +29,7 @@ use Utopia\Migration\Resource;
|
||||
use Utopia\Migration\Sources\Appwrite;
|
||||
use Utopia\Migration\Sources\CSV;
|
||||
use Utopia\Migration\Sources\Firebase;
|
||||
use Utopia\Migration\Sources\JSON;
|
||||
use Utopia\Migration\Sources\NHost;
|
||||
use Utopia\Migration\Sources\Supabase;
|
||||
use Utopia\Migration\Transfer;
|
||||
@@ -43,6 +44,25 @@ use Utopia\Validator\WhiteList;
|
||||
|
||||
include_once __DIR__ . '/../shared/api.php';
|
||||
|
||||
function getDatabaseTransferResourceServices(string $databaseType)
|
||||
{
|
||||
return match($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
|
||||
};
|
||||
}
|
||||
|
||||
function getDatabaseResourceType(string $databaseType): string
|
||||
{
|
||||
return match($databaseType) {
|
||||
DATABASE_TYPE_VECTORSDB => Resource::TYPE_DATABASE_VECTORSDB,
|
||||
DATABASE_TYPE_DOCUMENTSDB => Resource::TYPE_DATABASE_DOCUMENTSDB,
|
||||
default => Resource::TYPE_DATABASE,
|
||||
};
|
||||
}
|
||||
|
||||
Http::post('/v1/migrations/appwrite')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Create Appwrite migration')
|
||||
@@ -427,8 +447,17 @@ Http::post('/v1/migrations/csv/imports')
|
||||
throw new \Exception('Unable to copy file');
|
||||
}
|
||||
|
||||
// getting databasetype
|
||||
$resources = explode(':', $resourceId);
|
||||
$databaseId = $resources[0];
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
$databaseType = $database->getAttribute('type');
|
||||
if (!in_array($databaseType, CSV_ALLOWED_DATABASE_TYPES)) {
|
||||
throw new Exception(Exception::MIGRATION_DATABASE_TYPE_UNSUPPORTED, 'Database type not supported for csv');
|
||||
}
|
||||
$fileSize = $deviceForMigrations->getFileSize($newPath);
|
||||
$resources = Transfer::extractServices([Transfer::GROUP_DATABASES]);
|
||||
$resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
|
||||
$resourceType = getDatabaseResourceType($databaseType);
|
||||
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => $migrationId,
|
||||
@@ -438,7 +467,7 @@ Http::post('/v1/migrations/csv/imports')
|
||||
'destination' => Appwrite::getName(),
|
||||
'resources' => $resources,
|
||||
'resourceId' => $resourceId,
|
||||
'resourceType' => Resource::TYPE_DATABASE,
|
||||
'resourceType' => $resourceType,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => [],
|
||||
@@ -547,25 +576,41 @@ Http::post('/v1/migrations/csv/exports')
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
// getting databasetype
|
||||
$resources = explode(':', $resourceId);
|
||||
$databaseId = $resources[0];
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
$databaseType = $database->getAttribute('type');
|
||||
if (!in_array($databaseType, CSV_ALLOWED_DATABASE_TYPES)) {
|
||||
throw new Exception(Exception::MIGRATION_DATABASE_TYPE_UNSUPPORTED, 'Database type not supported for csv');
|
||||
}
|
||||
|
||||
// Schemaless databases (DocumentsDB, VectorsDB) allow queries on dynamic fields
|
||||
$isSchemaless = in_array($databaseType, [DATABASE_TYPE_DOCUMENTSDB, DATABASE_TYPE_VECTORSDB]);
|
||||
|
||||
$validator = new Documents(
|
||||
attributes: $collection->getAttribute('attributes', []),
|
||||
indexes: $collection->getAttribute('indexes', []),
|
||||
idAttributeType: $dbForProject->getAdapter()->getIdAttributeType(),
|
||||
supportForAttributes: !$isSchemaless,
|
||||
);
|
||||
|
||||
if (!$validator->isValid($parsedQueries)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
|
||||
$resourceType = getDatabaseResourceType($databaseType);
|
||||
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
'stage' => 'init',
|
||||
'source' => Appwrite::getName(),
|
||||
'destination' => CSV::getName(),
|
||||
'resources' => Transfer::extractServices([Transfer::GROUP_DATABASES]),
|
||||
'resources' => $resources,
|
||||
'resourceId' => $resourceId,
|
||||
'resourceType' => Resource::TYPE_DATABASE,
|
||||
'resourceType' => $resourceType,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => [],
|
||||
@@ -596,6 +641,291 @@ Http::post('/v1/migrations/csv/exports')
|
||||
->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
Http::post('/v1/migrations/json/imports')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Import documents from a JSON')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('event', 'migrations.[migrationId].create')
|
||||
->label('audits.event', 'migration.create')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'migrations',
|
||||
group: null,
|
||||
name: 'createJSONImport',
|
||||
description: '/docs/references/migrations/migration-json-import.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_ACCEPTED,
|
||||
model: Response::MODEL_MIGRATION,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).')
|
||||
->param('fileId', '', new UID(), 'File ID.')
|
||||
->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database.')
|
||||
->param('internalFile', false, new Boolean(), 'Is the file stored in an internal bucket?', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('deviceForFiles')
|
||||
->inject('deviceForMigrations')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (
|
||||
string $bucketId,
|
||||
string $fileId,
|
||||
string $resourceId,
|
||||
bool $internalFile,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
Document $project,
|
||||
array $platform,
|
||||
Device $deviceForFiles,
|
||||
Device $deviceForMigrations,
|
||||
Event $queueForEvents,
|
||||
Migration $queueForMigrations
|
||||
) {
|
||||
$bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
|
||||
if ($internalFile) {
|
||||
return $dbForPlatform->getDocument('buckets', 'default');
|
||||
}
|
||||
return $dbForProject->getDocument('buckets', $bucketId);
|
||||
});
|
||||
|
||||
if ($bucket->isEmpty()) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
$file = $authorization->skip(fn () => $internalFile ? $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $fileId) : $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
|
||||
if ($file->isEmpty()) {
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$path = $file->getAttribute('path', '');
|
||||
if (!$deviceForFiles->exists($path)) {
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND, 'File not found in ' . $path);
|
||||
}
|
||||
|
||||
// No encryption or compression on files above 20MB.
|
||||
$hasEncryption = !empty($file->getAttribute('openSSLCipher'));
|
||||
$compression = $file->getAttribute('algorithm', Compression::NONE);
|
||||
$hasCompression = $compression !== Compression::NONE;
|
||||
|
||||
$migrationId = ID::unique();
|
||||
$newPath = $deviceForMigrations->getPath($migrationId . '_' . $fileId . '.json');
|
||||
|
||||
if ($hasEncryption || $hasCompression) {
|
||||
$source = $deviceForFiles->read($path);
|
||||
|
||||
if ($hasEncryption) {
|
||||
$source = OpenSSL::decrypt(
|
||||
$source,
|
||||
$file->getAttribute('openSSLCipher'),
|
||||
System::getEnv('_APP_OPENSSL_KEY_V' . $file->getAttribute('openSSLVersion')),
|
||||
0,
|
||||
hex2bin($file->getAttribute('openSSLIV')),
|
||||
hex2bin($file->getAttribute('openSSLTag'))
|
||||
);
|
||||
}
|
||||
|
||||
if ($hasCompression) {
|
||||
switch ($compression) {
|
||||
case Compression::ZSTD:
|
||||
$source = (new Zstd())->decompress($source);
|
||||
break;
|
||||
case Compression::GZIP:
|
||||
$source = (new GZIP())->decompress($source);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Manual write after decryption and/or decompression
|
||||
if (!$deviceForMigrations->write($newPath, $source, 'application/json')) {
|
||||
throw new \Exception('Unable to copy file');
|
||||
}
|
||||
} elseif (!$deviceForFiles->transfer($path, $newPath, $deviceForMigrations)) {
|
||||
throw new \Exception('Unable to copy file');
|
||||
}
|
||||
|
||||
$fileSize = $deviceForMigrations->getFileSize($newPath);
|
||||
|
||||
[$databaseId] = \explode(':', $resourceId, 2);
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
}
|
||||
$databaseType = $database->getAttribute('type');
|
||||
$resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
|
||||
$resourceType = getDatabaseResourceType($databaseType);
|
||||
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => $migrationId,
|
||||
'status' => 'pending',
|
||||
'stage' => 'init',
|
||||
'source' => JSON::getName(),
|
||||
'destination' => Appwrite::getName(),
|
||||
'resources' => $resources,
|
||||
'resourceId' => $resourceId,
|
||||
'resourceType' => $resourceType,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => [],
|
||||
'options' => [
|
||||
'path' => $newPath,
|
||||
'size' => $fileSize,
|
||||
],
|
||||
]));
|
||||
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->trigger();
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
Http::post('/v1/migrations/json/exports')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Export documents to JSON')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('event', 'migrations.[migrationId].create')
|
||||
->label('audits.event', 'migration.create')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'migrations',
|
||||
group: null,
|
||||
name: 'createJSONExport',
|
||||
description: '/docs/references/migrations/migration-json-export.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_ACCEPTED,
|
||||
model: Response::MODEL_MIGRATION,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database to export.')
|
||||
->param('filename', '', new Text(255), 'The name of the file to be created for the export, excluding the .json extension.')
|
||||
->param('columns', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of attributes to export. If empty, all attributes will be exported. You can use the `*` wildcard to export all attributes from the collection.', true)
|
||||
->param('queries', [], new ArrayList(new Text(0)), 'Array of query strings generated using the Query class provided by the SDK to filter documents to export. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
|
||||
->param('notify', true, new Boolean(), 'Set to true to receive an email when the export is complete. Default is true.', true)
|
||||
->inject('user')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (
|
||||
string $resourceId,
|
||||
string $filename,
|
||||
array $columns,
|
||||
array $queries,
|
||||
bool $notify,
|
||||
Document $user,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
Document $project,
|
||||
array $platform,
|
||||
Event $queueForEvents,
|
||||
Migration $queueForMigrations
|
||||
) {
|
||||
try {
|
||||
$parsedQueries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$bucket = $authorization->skip(fn () => $dbForPlatform->getDocument('buckets', 'default'));
|
||||
if ($bucket->isEmpty()) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
[$databaseId, $collectionId] = \explode(':', $resourceId, 2);
|
||||
if (empty($databaseId)) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
}
|
||||
if (empty($collectionId)) {
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$databaseType = $database->getAttribute('type');
|
||||
|
||||
// Schemaless databases (DocumentsDB, VectorsDB) allow queries on dynamic fields
|
||||
$isSchemaless = in_array($databaseType, [DATABASE_TYPE_DOCUMENTSDB, DATABASE_TYPE_VECTORSDB]);
|
||||
|
||||
$validator = new Documents(
|
||||
attributes: $collection->getAttribute('attributes', []),
|
||||
indexes: $collection->getAttribute('indexes', []),
|
||||
idAttributeType: $dbForProject->getAdapter()->getIdAttributeType(),
|
||||
supportForAttributes: !$isSchemaless,
|
||||
);
|
||||
|
||||
if (!$validator->isValid($parsedQueries)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
|
||||
$resourceType = getDatabaseResourceType($databaseType);
|
||||
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
'stage' => 'init',
|
||||
'source' => Appwrite::getName(),
|
||||
'destination' => JSON::getName(),
|
||||
'resources' => $resources,
|
||||
'resourceId' => $resourceId,
|
||||
'resourceType' => $resourceType,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => [],
|
||||
'options' => [
|
||||
'bucketId' => 'default', // Always use internal bucket
|
||||
'filename' => $filename,
|
||||
'columns' => $columns,
|
||||
'queries' => $queries,
|
||||
'notify' => $notify,
|
||||
'userInternalId' => $user->getSequence(),
|
||||
],
|
||||
]));
|
||||
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->trigger();
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
Http::get('/v1/migrations')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('List migrations')
|
||||
@@ -713,12 +1043,11 @@ Http::get('/v1/migrations/appwrite/report')
|
||||
->param('projectID', '', new Text(512), "Source's Project ID")
|
||||
->param('key', '', new Text(512), "Source's API Key")
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response) {
|
||||
->inject('getDatabasesDB')
|
||||
->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response, callable $getDatabasesDB) {
|
||||
|
||||
try {
|
||||
$appwrite = new Appwrite($projectID, $endpoint, $key);
|
||||
$appwrite = new Appwrite($projectID, $endpoint, $key, $getDatabasesDB);
|
||||
$report = $appwrite->report($resources);
|
||||
} catch (\Throwable $e) {
|
||||
throw new Exception(
|
||||
|
||||
+60
-235
@@ -1,25 +1,15 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Datetime as DateTimeValidator;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
Http::get('/v1/project/usage')
|
||||
@@ -62,16 +52,33 @@ Http::get('/v1/project/usage')
|
||||
METRIC_EXECUTIONS_MB_SECONDS,
|
||||
METRIC_BUILDS_MB_SECONDS,
|
||||
METRIC_DOCUMENTS,
|
||||
METRIC_DOCUMENTS_DOCUMENTSDB,
|
||||
METRIC_DATABASES,
|
||||
METRIC_DATABASES_DOCUMENTSDB,
|
||||
METRIC_USERS,
|
||||
METRIC_BUCKETS,
|
||||
METRIC_FILES_STORAGE,
|
||||
METRIC_DATABASES_STORAGE,
|
||||
METRIC_DATABASES_STORAGE_DOCUMENTSDB,
|
||||
METRIC_DEPLOYMENTS_STORAGE,
|
||||
METRIC_BUILDS_STORAGE,
|
||||
METRIC_DATABASES_OPERATIONS_READS,
|
||||
METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB,
|
||||
METRIC_DATABASES_OPERATIONS_WRITES,
|
||||
METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB,
|
||||
METRIC_FILES_IMAGES_TRANSFORMED,
|
||||
// VectorsDB totals
|
||||
METRIC_DATABASES_VECTORSDB,
|
||||
METRIC_COLLECTIONS_VECTORSDB,
|
||||
METRIC_DOCUMENTS_VECTORSDB,
|
||||
METRIC_DATABASES_STORAGE_VECTORSDB,
|
||||
METRIC_DATABASES_OPERATIONS_READS_VECTORSDB,
|
||||
METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB,
|
||||
// Embeddings totals
|
||||
METRIC_EMBEDDINGS_TEXT,
|
||||
METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS,
|
||||
METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION,
|
||||
METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR
|
||||
],
|
||||
'period' => [
|
||||
METRIC_NETWORK_REQUESTS,
|
||||
@@ -80,11 +87,26 @@ Http::get('/v1/project/usage')
|
||||
METRIC_USERS,
|
||||
METRIC_EXECUTIONS,
|
||||
METRIC_DATABASES_STORAGE,
|
||||
METRIC_DATABASES_STORAGE_DOCUMENTSDB,
|
||||
METRIC_EXECUTIONS_MB_SECONDS,
|
||||
METRIC_BUILDS_MB_SECONDS,
|
||||
METRIC_DATABASES_OPERATIONS_READS,
|
||||
METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB,
|
||||
METRIC_DATABASES_OPERATIONS_WRITES,
|
||||
METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB,
|
||||
METRIC_FILES_IMAGES_TRANSFORMED,
|
||||
// VectorsDB time series
|
||||
METRIC_DATABASES_VECTORSDB,
|
||||
METRIC_COLLECTIONS_VECTORSDB,
|
||||
METRIC_DOCUMENTS_VECTORSDB,
|
||||
METRIC_DATABASES_STORAGE_VECTORSDB,
|
||||
METRIC_DATABASES_OPERATIONS_READS_VECTORSDB,
|
||||
METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB,
|
||||
// Embeddings time series
|
||||
METRIC_EMBEDDINGS_TEXT,
|
||||
METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS,
|
||||
METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION,
|
||||
METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR
|
||||
]
|
||||
];
|
||||
|
||||
@@ -357,8 +379,11 @@ Http::get('/v1/project/usage')
|
||||
'buildsMbSecondsTotal' => $total[METRIC_BUILDS_MB_SECONDS],
|
||||
'documentsTotal' => $total[METRIC_DOCUMENTS],
|
||||
'rowsTotal' => $total[METRIC_DOCUMENTS],
|
||||
'documentsdbDocumentsTotal' => $total[METRIC_DOCUMENTS_DOCUMENTSDB],
|
||||
'databasesTotal' => $total[METRIC_DATABASES],
|
||||
'documentsdbTotal' => $total[METRIC_DATABASES_DOCUMENTSDB],
|
||||
'databasesStorageTotal' => $total[METRIC_DATABASES_STORAGE],
|
||||
'documentsdbDatabasesStorageTotal' => $total[METRIC_DATABASES_STORAGE_DOCUMENTSDB],
|
||||
'usersTotal' => $total[METRIC_USERS],
|
||||
'bucketsTotal' => $total[METRIC_BUCKETS],
|
||||
'filesStorageTotal' => $total[METRIC_FILES_STORAGE],
|
||||
@@ -367,10 +392,27 @@ Http::get('/v1/project/usage')
|
||||
'deploymentsStorageTotal' => $total[METRIC_DEPLOYMENTS_STORAGE],
|
||||
'databasesReadsTotal' => $total[METRIC_DATABASES_OPERATIONS_READS],
|
||||
'databasesWritesTotal' => $total[METRIC_DATABASES_OPERATIONS_WRITES],
|
||||
'documentsdbDatabasesReadsTotal' => $total[METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB],
|
||||
'documentsdbDatabasesWritesTotal' => $total[METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB],
|
||||
'vectorsdbDatabasesTotal' => $total[METRIC_DATABASES_VECTORSDB] ?? 0,
|
||||
'vectorsdbCollectionsTotal' => $total[METRIC_COLLECTIONS_VECTORSDB] ?? 0,
|
||||
'vectorsdbDocumentsTotal' => $total[METRIC_DOCUMENTS_VECTORSDB] ?? 0,
|
||||
'vectorsdbDatabasesStorageTotal' => $total[METRIC_DATABASES_STORAGE_VECTORSDB] ?? 0,
|
||||
'vectorsdbDatabasesReadsTotal' => $total[METRIC_DATABASES_OPERATIONS_READS_VECTORSDB] ?? 0,
|
||||
'vectorsdbDatabasesWritesTotal' => $total[METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB] ?? 0,
|
||||
'executionsBreakdown' => $executionsBreakdown,
|
||||
'bucketsBreakdown' => $bucketsBreakdown,
|
||||
'databasesReads' => $usage[METRIC_DATABASES_OPERATIONS_READS],
|
||||
'databasesWrites' => $usage[METRIC_DATABASES_OPERATIONS_WRITES],
|
||||
'documentsdbDatabasesReads' => $usage[METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB],
|
||||
'documentsdbDatabasesWrites' => $usage[METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB],
|
||||
'documentsdbDatabasesStorage' => $usage[METRIC_DATABASES_STORAGE_DOCUMENTSDB],
|
||||
'vectorsdbDatabases' => $usage[METRIC_DATABASES_VECTORSDB] ?? [],
|
||||
'vectorsdbCollections' => $usage[METRIC_COLLECTIONS_VECTORSDB] ?? [],
|
||||
'vectorsdbDocuments' => $usage[METRIC_DOCUMENTS_VECTORSDB] ?? [],
|
||||
'vectorsdbDatabasesStorage' => $usage[METRIC_DATABASES_STORAGE_VECTORSDB] ?? [],
|
||||
'vectorsdbDatabasesReads' => $usage[METRIC_DATABASES_OPERATIONS_READS_VECTORSDB] ?? [],
|
||||
'vectorsdbDatabasesWrites' => $usage[METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB] ?? [],
|
||||
'databasesStorageBreakdown' => $databasesStorageBreakdown,
|
||||
'executionsMbSecondsBreakdown' => $executionsMbSecondsBreakdown,
|
||||
'buildsMbSecondsBreakdown' => $buildsMbSecondsBreakdown,
|
||||
@@ -380,230 +422,13 @@ Http::get('/v1/project/usage')
|
||||
'authPhoneCountryBreakdown' => $authPhoneCountryBreakdown,
|
||||
'imageTransformations' => $usage[METRIC_FILES_IMAGES_TRANSFORMED],
|
||||
'imageTransformationsTotal' => $total[METRIC_FILES_IMAGES_TRANSFORMED],
|
||||
'embeddingsText' => $usage[METRIC_EMBEDDINGS_TEXT] ?? [],
|
||||
'embeddingsTextTokens' => $usage[METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS] ?? [],
|
||||
'embeddingsTextDuration' => $usage[METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION] ?? [],
|
||||
'embeddingsTextErrors' => $usage[METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR] ?? [],
|
||||
'embeddingsTextTotal' => $total[METRIC_EMBEDDINGS_TEXT] ?? 0,
|
||||
'embeddingsTextTokensTotal' => $total[METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS] ?? 0,
|
||||
'embeddingsTextDurationTotal' => $total[METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION] ?? 0,
|
||||
'embeddingsTextErrorsTotal' => $total[METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR] ?? 0,
|
||||
]), Response::MODEL_USAGE_PROJECT);
|
||||
});
|
||||
|
||||
|
||||
// Variables
|
||||
Http::post('/v1/project/variables')
|
||||
->desc('Create variable')
|
||||
->groups(['api'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('audits.event', 'variable.create')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: null,
|
||||
name: 'createVariable',
|
||||
description: '/docs/references/project/create-variable.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
model: Response::MODEL_VARIABLE,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false)
|
||||
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
|
||||
->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
|
||||
->inject('project')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $key, string $value, bool $secret, Document $project, Response $response, Database $dbForProject, Database $dbForPlatform) {
|
||||
$variableId = ID::unique();
|
||||
|
||||
$variable = new Document([
|
||||
'$id' => $variableId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'resourceInternalId' => '',
|
||||
'resourceId' => '',
|
||||
'resourceType' => 'project',
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'secret' => $secret,
|
||||
'search' => implode(' ', [$variableId, $key, 'project']),
|
||||
]);
|
||||
|
||||
try {
|
||||
$variable = $dbForProject->createDocument('variables', $variable);
|
||||
} catch (DuplicateException $th) {
|
||||
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$functions = $dbForProject->find('functions', [
|
||||
Query::limit(APP_LIMIT_SUBQUERY)
|
||||
]);
|
||||
|
||||
foreach ($functions as $function) {
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
|
||||
}
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($variable, Response::MODEL_VARIABLE);
|
||||
});
|
||||
|
||||
Http::get('/v1/project/variables')
|
||||
->desc('List variables')
|
||||
->groups(['api'])
|
||||
->label('scope', 'projects.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: null,
|
||||
name: 'listVariables',
|
||||
description: '/docs/references/project/list-variables.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_VARIABLE_LIST,
|
||||
)
|
||||
]
|
||||
))
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (Response $response, Database $dbForProject) {
|
||||
$variables = $dbForProject->find('variables', [
|
||||
Query::equal('resourceType', ['project']),
|
||||
Query::limit(APP_LIMIT_SUBQUERY)
|
||||
]);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'variables' => $variables,
|
||||
'total' => \count($variables),
|
||||
]), Response::MODEL_VARIABLE_LIST);
|
||||
});
|
||||
|
||||
Http::get('/v1/project/variables/:variableId')
|
||||
->desc('Get variable')
|
||||
->groups(['api'])
|
||||
->label('scope', 'projects.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: null,
|
||||
name: 'getVariable',
|
||||
description: '/docs/references/project/get-variable.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_VARIABLE,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $variableId, Response $response, Document $project, Database $dbForProject) {
|
||||
$variable = $dbForProject->getDocument('variables', $variableId);
|
||||
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($variable, Response::MODEL_VARIABLE);
|
||||
});
|
||||
|
||||
Http::put('/v1/project/variables/:variableId')
|
||||
->desc('Update variable')
|
||||
->groups(['api'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: null,
|
||||
name: 'updateVariable',
|
||||
description: '/docs/references/project/update-variable.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_VARIABLE,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
|
||||
->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false)
|
||||
->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true)
|
||||
->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
|
||||
->inject('project')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $variableId, string $key, ?string $value, ?bool $secret, Document $project, Response $response, Database $dbForProject, Database $dbForPlatform) {
|
||||
$variable = $dbForProject->getDocument('variables', $variableId);
|
||||
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($variable->getAttribute('secret') === true && $secret === false) {
|
||||
throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET);
|
||||
}
|
||||
|
||||
$variable
|
||||
->setAttribute('key', $key)
|
||||
->setAttribute('value', $value ?? $variable->getAttribute('value'))
|
||||
->setAttribute('secret', $secret ?? $variable->getAttribute('secret'))
|
||||
->setAttribute('search', implode(' ', [$variableId, $key, 'project']));
|
||||
|
||||
try {
|
||||
$dbForProject->updateDocument('variables', $variable->getId(), $variable);
|
||||
} catch (DuplicateException $th) {
|
||||
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$functions = $dbForProject->find('functions', [
|
||||
Query::limit(APP_LIMIT_SUBQUERY)
|
||||
]);
|
||||
|
||||
foreach ($functions as $function) {
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
|
||||
}
|
||||
|
||||
$response->dynamic($variable, Response::MODEL_VARIABLE);
|
||||
});
|
||||
|
||||
Http::delete('/v1/project/variables/:variableId')
|
||||
->desc('Delete variable')
|
||||
->groups(['api'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: null,
|
||||
name: 'deleteVariable',
|
||||
description: '/docs/references/project/delete-variable.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_NOCONTENT,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::NONE
|
||||
))
|
||||
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
|
||||
->inject('project')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $variableId, Document $project, Response $response, Database $dbForProject) {
|
||||
$variable = $dbForProject->getDocument('variables', $variableId);
|
||||
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$dbForProject->deleteDocument('variables', $variable->getId());
|
||||
|
||||
$functions = $dbForProject->find('functions', [
|
||||
Query::limit(APP_LIMIT_SUBQUERY)
|
||||
]);
|
||||
|
||||
foreach ($functions as $function) {
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
|
||||
}
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
@@ -4,10 +4,8 @@ use Ahc\Jwt\JWT;
|
||||
use Appwrite\Auth\Validator\MockNumber;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Validator\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
@@ -30,7 +28,7 @@ use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Domains\Validator\PublicDomain;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\System\System;
|
||||
@@ -38,11 +36,9 @@ use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Hostname;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Multiple;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\URL;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
Http::init()
|
||||
@@ -773,312 +769,6 @@ Http::delete('/v1/projects/:projectId')
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
// Webhooks
|
||||
|
||||
Http::post('/v1/projects/:projectId/webhooks')
|
||||
->desc('Create webhook')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'webhooks',
|
||||
name: 'createWebhook',
|
||||
description: '/docs/references/projects/create-webhook.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
model: Response::MODEL_WEBHOOK,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.')
|
||||
->param('enabled', true, new Boolean(true), 'Enable or disable a webhook.', true)
|
||||
->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
|
||||
->param('url', '', fn ($request) => new Multiple([new URL(['http', 'https']), new PublicDomain()], Multiple::TYPE_STRING), 'Webhook URL.', false, ['request'])
|
||||
->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.')
|
||||
->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
|
||||
->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$security = (bool) filter_var($security, FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
$webhook = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'projectInternalId' => $project->getSequence(),
|
||||
'projectId' => $project->getId(),
|
||||
'name' => $name,
|
||||
'events' => $events,
|
||||
'url' => $url,
|
||||
'security' => $security,
|
||||
'httpUser' => $httpUser,
|
||||
'httpPass' => $httpPass,
|
||||
'signatureKey' => \bin2hex(\random_bytes(64)),
|
||||
'enabled' => $enabled,
|
||||
]);
|
||||
|
||||
$webhook = $dbForPlatform->createDocument('webhooks', $webhook);
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($webhook, Response::MODEL_WEBHOOK);
|
||||
});
|
||||
|
||||
Http::get('/v1/projects/:projectId/webhooks')
|
||||
->desc('List webhooks')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'webhooks',
|
||||
name: 'listWebhooks',
|
||||
description: '/docs/references/projects/list-webhooks.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_WEBHOOK_LIST,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$webhooks = $dbForPlatform->find('webhooks', [
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
Query::limit(5000),
|
||||
]);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'webhooks' => $webhooks,
|
||||
'total' => $includeTotal ? count($webhooks) : 0,
|
||||
]), Response::MODEL_WEBHOOK_LIST);
|
||||
});
|
||||
|
||||
Http::get('/v1/projects/:projectId/webhooks/:webhookId')
|
||||
->desc('Get webhook')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'webhooks',
|
||||
name: 'getWebhook',
|
||||
description: '/docs/references/projects/get-webhook.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_WEBHOOK,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook unique ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $webhookId, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$webhook = $dbForPlatform->findOne('webhooks', [
|
||||
Query::equal('$id', [$webhookId]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($webhook->isEmpty()) {
|
||||
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
|
||||
});
|
||||
|
||||
Http::put('/v1/projects/:projectId/webhooks/:webhookId')
|
||||
->desc('Update webhook')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'webhooks',
|
||||
name: 'updateWebhook',
|
||||
description: '/docs/references/projects/update-webhook.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_WEBHOOK,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook unique ID.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.')
|
||||
->param('enabled', true, new Boolean(true), 'Enable or disable a webhook.', true)
|
||||
->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
|
||||
->param('url', '', fn ($request) => new Multiple([new URL(['http', 'https']), new PublicDomain()], Multiple::TYPE_STRING), 'Webhook URL.', false, ['request'])
|
||||
->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.')
|
||||
->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
|
||||
->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $webhookId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$security = ($security === '1' || $security === 'true' || $security === 1 || $security === true);
|
||||
|
||||
$webhook = $dbForPlatform->findOne('webhooks', [
|
||||
Query::equal('$id', [$webhookId]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($webhook->isEmpty()) {
|
||||
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
|
||||
}
|
||||
|
||||
$webhook
|
||||
->setAttribute('name', $name)
|
||||
->setAttribute('events', $events)
|
||||
->setAttribute('url', $url)
|
||||
->setAttribute('security', $security)
|
||||
->setAttribute('httpUser', $httpUser)
|
||||
->setAttribute('httpPass', $httpPass)
|
||||
->setAttribute('enabled', $enabled);
|
||||
|
||||
if ($enabled) {
|
||||
$webhook->setAttribute('attempts', 0);
|
||||
}
|
||||
|
||||
$dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook);
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
|
||||
});
|
||||
|
||||
Http::patch('/v1/projects/:projectId/webhooks/:webhookId/signature')
|
||||
->desc('Update webhook signature key')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'webhooks',
|
||||
name: 'updateWebhookSignature',
|
||||
description: '/docs/references/projects/update-webhook-signature.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_WEBHOOK,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook unique ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $webhookId, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$webhook = $dbForPlatform->findOne('webhooks', [
|
||||
Query::equal('$id', [$webhookId]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($webhook->isEmpty()) {
|
||||
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
|
||||
}
|
||||
|
||||
$webhook->setAttribute('signatureKey', \bin2hex(\random_bytes(64)));
|
||||
|
||||
$dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook);
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
|
||||
});
|
||||
|
||||
Http::delete('/v1/projects/:projectId/webhooks/:webhookId')
|
||||
->desc('Delete webhook')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'webhooks',
|
||||
name: 'deleteWebhook',
|
||||
description: '/docs/references/projects/delete-webhook.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_NOCONTENT,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::NONE
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook unique ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $webhookId, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$webhook = $dbForPlatform->findOne('webhooks', [
|
||||
Query::equal('$id', [$webhookId]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($webhook->isEmpty()) {
|
||||
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
|
||||
}
|
||||
|
||||
$dbForPlatform->deleteDocument('webhooks', $webhook->getId());
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
// Keys
|
||||
|
||||
Http::post('/v1/projects/:projectId/keys')
|
||||
@@ -1880,7 +1570,7 @@ Http::post('/v1/projects/:projectId/smtp/tests')
|
||||
->trigger();
|
||||
}
|
||||
|
||||
return $response->noContent();
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
Http::get('/v1/projects/:projectId/templates/sms/:type/:locale')
|
||||
|
||||
+146
-42
@@ -15,7 +15,6 @@ use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Hooks\Hooks;
|
||||
use Appwrite\Network\Validator\Email as EmailValidator;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
@@ -60,6 +59,7 @@ use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Emails\Email;
|
||||
use Utopia\Emails\Validator\Email as EmailValidator;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\System\System;
|
||||
@@ -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)
|
||||
@@ -1212,6 +1248,47 @@ Http::put('/v1/users/:userId/labels')
|
||||
$response->dynamic($user, Response::MODEL_USER);
|
||||
});
|
||||
|
||||
Http::patch('/v1/users/:userId/impersonator')
|
||||
->desc('Update user impersonator capability')
|
||||
->groups(['api', 'users'])
|
||||
->label('event', 'users.[userId].update.impersonator')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'user.update')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'users',
|
||||
group: 'users',
|
||||
name: 'updateImpersonator',
|
||||
description: '/docs/references/users/update-user-impersonator.md',
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_USER,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('userId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'User ID.', false, ['dbForProject'])
|
||||
->param('impersonator', false, new Boolean(true), 'Whether the user can impersonate other users. When true, the user can browse project users to choose a target and can pass impersonation headers to act as that user. Internal audit logs still attribute impersonated actions to the original impersonator and store the target user details only in internal audit payload data.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, bool $impersonator, Response $response, Database $dbForProject, Event $queueForEvents) {
|
||||
|
||||
$user = $dbForProject->getDocument('users', $userId);
|
||||
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['impersonator' => $impersonator]));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId());
|
||||
|
||||
$response->dynamic($user, Response::MODEL_USER);
|
||||
});
|
||||
|
||||
Http::patch('/v1/users/:userId/verification/phone')
|
||||
->desc('Update phone verification')
|
||||
->groups(['api', 'users'])
|
||||
@@ -1429,8 +1506,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);
|
||||
|
||||
@@ -1461,20 +1540,47 @@ Http::patch('/v1/users/:userId/email')
|
||||
|
||||
$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 {
|
||||
@@ -2296,9 +2402,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')
|
||||
@@ -2361,9 +2466,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')
|
||||
@@ -2617,7 +2721,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')
|
||||
|
||||
+51
-13
@@ -166,14 +166,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 {
|
||||
@@ -244,6 +244,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 +274,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)) {
|
||||
@@ -379,7 +380,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'] = '';
|
||||
@@ -459,7 +460,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
if ($version === 'v2') {
|
||||
$vars = \array_merge($vars, [
|
||||
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
|
||||
'APPWRITE_FUNCTION_DATA' => $body ?? '',
|
||||
'APPWRITE_FUNCTION_DATA' => $body,
|
||||
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
|
||||
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
|
||||
]);
|
||||
@@ -529,6 +530,11 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
}
|
||||
|
||||
/** Execute function */
|
||||
$executionResponse = [
|
||||
'headers' => [],
|
||||
'body' => '',
|
||||
];
|
||||
|
||||
try {
|
||||
$version = match ($type) {
|
||||
'function' => $resource->getAttribute('version', 'v2'),
|
||||
@@ -734,7 +740,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) {
|
||||
@@ -862,12 +868,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', ''));
|
||||
@@ -973,7 +979,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 +1019,7 @@ Http::init()
|
||||
return;
|
||||
}
|
||||
$route = $request->getRoute();
|
||||
if ($route->getLabel('origin', false) === '*') {
|
||||
if ($route?->getLabel('origin', false) === '*') {
|
||||
return;
|
||||
}
|
||||
if (!$originValidator->isValid($origin)) {
|
||||
@@ -1190,6 +1197,15 @@ 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);
|
||||
@@ -1261,7 +1277,16 @@ Http::error()
|
||||
* If not a publishable error, track usage stats. Publishable errors are >= 500 or those explicitly marked as publish=true in errors.php
|
||||
*/
|
||||
if (!$publish && $project->getId() !== 'console') {
|
||||
if (!DBUser::isPrivileged($authorization->getRoles())) {
|
||||
$errorUser = new DBUser();
|
||||
try {
|
||||
$resolvedUser = $utopia->getResource('user');
|
||||
if ($resolvedUser instanceof DBUser) {
|
||||
$errorUser = $resolvedUser;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// User resource may not be available in error context
|
||||
}
|
||||
if (!$errorUser->isPrivileged($authorization->getRoles())) {
|
||||
$bus->dispatch(new RequestCompleted(
|
||||
project: $project->getArrayCopy(),
|
||||
request: $request,
|
||||
@@ -1468,6 +1493,19 @@ 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->addHeader($name, $value, override: true);
|
||||
}
|
||||
} 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')
|
||||
|
||||
+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(),
|
||||
]);
|
||||
|
||||
@@ -96,7 +96,7 @@ Http::init()
|
||||
->inject('team')
|
||||
->inject('apiKey')
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $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) {
|
||||
$route = $utopia->getRoute();
|
||||
|
||||
/**
|
||||
@@ -338,6 +338,19 @@ Http::init()
|
||||
|
||||
$scopes = \array_unique($scopes);
|
||||
|
||||
// Intentional: impersonators get users.read so they can discover a target user
|
||||
// before impersonation starts, and keep that access while impersonating.
|
||||
if (
|
||||
!$user->isEmpty()
|
||||
&& (
|
||||
$user->getAttribute('impersonator', false)
|
||||
|| $user->getAttribute('impersonatorUserId')
|
||||
)
|
||||
) {
|
||||
$scopes[] = 'users.read';
|
||||
$scopes = \array_unique($scopes);
|
||||
}
|
||||
|
||||
$authorization->addRole($role);
|
||||
foreach ($user->getRoles($authorization) as $authRole) {
|
||||
$authorization->addRole($authRole);
|
||||
@@ -369,8 +382,11 @@ Http::init()
|
||||
}
|
||||
|
||||
if (! empty($user->getId())) {
|
||||
$impersonatorUserId = $user->getAttribute('impersonatorUserId');
|
||||
$accessedAt = $user->getAttribute('accessedAt', 0);
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
|
||||
|
||||
// Skip updating accessedAt for impersonated requests so we don't attribute activity to the target user.
|
||||
if (! $impersonatorUserId && DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
|
||||
$user->setAttribute('accessedAt', DateTime::now());
|
||||
|
||||
if ($project->getId() !== 'console' && $mode !== APP_MODE_ADMIN) {
|
||||
@@ -403,7 +419,7 @@ Http::init()
|
||||
if (
|
||||
array_key_exists($namespace, $project->getAttribute('services', []))
|
||||
&& ! $project->getAttribute('services', [])[$namespace]
|
||||
&& ! (User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
|
||||
}
|
||||
@@ -467,14 +483,23 @@ Http::init()
|
||||
->inject('telemetry')
|
||||
->inject('platform')
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, Document $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, 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) {
|
||||
|
||||
$response->setUser($user);
|
||||
$request->setUser($user);
|
||||
|
||||
$route = $utopia->getRoute();
|
||||
$path = $route->getMatchedPath();
|
||||
$databaseType = match (true) {
|
||||
str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
|
||||
str_contains($path, '/vectorsdb') => DATABASE_TYPE_VECTORSDB,
|
||||
default => '',
|
||||
};
|
||||
|
||||
if (
|
||||
array_key_exists('rest', $project->getAttribute('apis', []))
|
||||
&& ! $project->getAttribute('apis', [])['rest']
|
||||
&& ! (User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
@@ -506,8 +531,8 @@ Http::init()
|
||||
$closestLimit = null;
|
||||
|
||||
$roles = $authorization->getRoles();
|
||||
$isPrivilegedUser = User::isPrivileged($roles);
|
||||
$isAppUser = User::isApp($roles);
|
||||
$isPrivilegedUser = $user->isPrivileged($roles);
|
||||
$isAppUser = $user->isApp($roles);
|
||||
|
||||
foreach ($timeLimitArray as $timeLimit) {
|
||||
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
|
||||
@@ -589,7 +614,7 @@ Http::init()
|
||||
if ($useCache) {
|
||||
$route = $utopia->match($request);
|
||||
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
|
||||
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! User::isPrivileged($authorization->getRoles());
|
||||
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$key = $request->cacheIdentifier();
|
||||
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
@@ -608,7 +633,7 @@ Http::init()
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isToken = ! $resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (! $bucket->getAttribute('enabled') && ! $isAppUser && ! $isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
@@ -641,7 +666,7 @@ Http::init()
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
// Do not update transformedAt if it's a console user
|
||||
if (! User::isPrivileged($authorization->getRoles())) {
|
||||
if (! $user->isPrivileged($authorization->getRoles())) {
|
||||
$transformedAt = $file->getAttribute('transformedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
|
||||
$file->setAttribute('transformedAt', DateTime::now());
|
||||
@@ -675,7 +700,7 @@ Http::init()
|
||||
->groups(['session'])
|
||||
->inject('user')
|
||||
->inject('request')
|
||||
->action(function (Document $user, Request $request) {
|
||||
->action(function (User $user, Request $request) {
|
||||
if (\str_contains($request->getURI(), 'oauth2')) {
|
||||
return;
|
||||
}
|
||||
@@ -962,7 +987,7 @@ Http::shutdown()
|
||||
}
|
||||
|
||||
if ($project->getId() !== 'console') {
|
||||
if (! User::isPrivileged($authorization->getRoles())) {
|
||||
if (! $user->isPrivileged($authorization->getRoles())) {
|
||||
$bus->dispatch(new RequestCompleted(
|
||||
project: $project->getArrayCopy(),
|
||||
request: $request,
|
||||
|
||||
@@ -36,8 +36,9 @@ Http::init()
|
||||
->inject('request')
|
||||
->inject('project')
|
||||
->inject('geodb')
|
||||
->inject('user')
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Document $project, Reader $geodb, Authorization $authorization) {
|
||||
->action(function (Http $utopia, Request $request, Document $project, Reader $geodb, User $user, Authorization $authorization) {
|
||||
$denylist = System::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', '');
|
||||
if (!empty($denylist && $project->getId() === 'console')) {
|
||||
$countries = explode(',', $denylist);
|
||||
@@ -50,8 +51,8 @@ Http::init()
|
||||
|
||||
$route = $utopia->match($request);
|
||||
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAppUser = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
$isAppUser = $user->isApp($authorization->getRoles());
|
||||
|
||||
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
|
||||
return;
|
||||
|
||||
+24
-5
@@ -3,6 +3,8 @@
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
require_once __DIR__ . '/init/span.php';
|
||||
|
||||
global $register;
|
||||
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Swoole\Constant;
|
||||
@@ -31,7 +33,6 @@ 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;
|
||||
|
||||
@@ -103,7 +104,7 @@ function dispatch(Server $server, int $fd, int $type, $data = null): int
|
||||
$lines = explode("\n", $data, 3);
|
||||
$request = $lines[0];
|
||||
if (count($lines) > 1) {
|
||||
$domain = trim(explode('Host: ', $lines[1])[1]);
|
||||
$domain = trim(explode('Host: ', $lines[1])[1] ?? '');
|
||||
}
|
||||
|
||||
// Sync executions are considered risky
|
||||
@@ -196,6 +197,8 @@ 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;
|
||||
@@ -291,7 +294,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $tot
|
||||
|
||||
go(function () use ($register, $app) {
|
||||
$pools = $register->get('pools');
|
||||
/** @var Group $pools */
|
||||
/** @var \Utopia\Pools\Group $pools */
|
||||
Http::setResource('pools', fn () => $pools);
|
||||
|
||||
/** @var array $collections */
|
||||
@@ -409,13 +412,29 @@ $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');
|
||||
|
||||
foreach ($sharedTablesV2 as $hostname) {
|
||||
// All shared tables V2 pools that need project metadata collections
|
||||
$sharedTablesV2All = \array_values(\array_unique(\array_filter([
|
||||
...$sharedTablesV2,
|
||||
...$documentsSharedTablesV2,
|
||||
...$vectorSharedTablesV2,
|
||||
])));
|
||||
|
||||
foreach ($sharedTablesV2All as $hostname) {
|
||||
Span::init('database.setup');
|
||||
Span::add('database.hostname', $hostname);
|
||||
|
||||
@@ -637,7 +656,7 @@ $http->on(Constant::EVENT_TASK, function () use ($register) {
|
||||
/** @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) {
|
||||
|
||||
+55
-1
@@ -47,7 +47,7 @@ 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.8.1';
|
||||
const APP_VERSION_STABLE = '1.9.0';
|
||||
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
|
||||
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
|
||||
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
|
||||
@@ -97,6 +97,7 @@ const APP_COMPUTE_DEPLOYMENT_MAX_RETENTION = 100 * 365; // 100 years
|
||||
const APP_SDK_PLATFORM_SERVER = 'server';
|
||||
const APP_SDK_PLATFORM_CLIENT = 'client';
|
||||
const APP_SDK_PLATFORM_CONSOLE = 'console';
|
||||
const APP_SDK_PLATFORM_STATIC = 'static';
|
||||
const APP_VCS_GITHUB_USERNAME = 'Appwrite';
|
||||
const APP_VCS_GITHUB_EMAIL = 'team@appwrite.io';
|
||||
const APP_VCS_GITHUB_URL = 'https://github.com/TeamAppwrite';
|
||||
@@ -288,6 +289,45 @@ const METRIC_DATABASES_OPERATIONS_READS = 'databases.operations.reads';
|
||||
const METRIC_DATABASE_ID_OPERATIONS_READS = '{databaseInternalId}.databases.operations.reads';
|
||||
const METRIC_DATABASES_OPERATIONS_WRITES = 'databases.operations.writes';
|
||||
const METRIC_DATABASE_ID_OPERATIONS_WRITES = '{databaseInternalId}.databases.operations.writes';
|
||||
|
||||
// documentsdb
|
||||
const METRIC_DATABASES_DOCUMENTSDB = 'documentsdb.databases';
|
||||
const METRIC_COLLECTIONS_DOCUMENTSDB = 'documentsdb.collections';
|
||||
const METRIC_DATABASES_STORAGE_DOCUMENTSDB = 'documentsdb.databases.storage';
|
||||
const METRIC_DATABASE_ID_COLLECTIONS_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.collections';
|
||||
const METRIC_DATABASE_ID_STORAGE_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.databases.storage';
|
||||
const METRIC_DOCUMENTS_DOCUMENTSDB = 'documentsdb.documents';
|
||||
const METRIC_DATABASE_ID_DOCUMENTS_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.documents';
|
||||
const METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.{collectionInternalId}.documents';
|
||||
const METRIC_DATABASE_ID_COLLECTION_ID_STORAGE_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.{collectionInternalId}.databases.storage';
|
||||
const METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB = 'documentsdb.databases.operations.reads';
|
||||
const METRIC_DATABASE_ID_OPERATIONS_READS_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.databases.operations.reads';
|
||||
const METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB = 'documentsdb.databases.operations.writes';
|
||||
const METRIC_DATABASE_ID_OPERATIONS_WRITES_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.databases.operations.writes';
|
||||
|
||||
// vectorsdb
|
||||
const METRIC_DATABASES_VECTORSDB = 'vectorsdb.databases';
|
||||
const METRIC_COLLECTIONS_VECTORSDB = 'vectorsdb.collections';
|
||||
const METRIC_DATABASES_STORAGE_VECTORSDB = 'vectorsdb.databases.storage';
|
||||
const METRIC_DATABASE_ID_COLLECTIONS_VECTORSDB = 'vectorsdb.{databaseInternalId}.collections';
|
||||
const METRIC_DATABASE_ID_STORAGE_VECTORSDB = 'vectorsdb.{databaseInternalId}.databases.storage';
|
||||
const METRIC_DOCUMENTS_VECTORSDB = 'vectorsdb.documents';
|
||||
const METRIC_DATABASE_ID_DOCUMENTS_VECTORSDB = 'vectorsdb.{databaseInternalId}.documents';
|
||||
const METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS_VECTORSDB = 'vectorsdb.{databaseInternalId}.{collectionInternalId}.documents';
|
||||
const METRIC_DATABASE_ID_COLLECTION_ID_STORAGE_VECTORSDB = 'vectorsdb.{databaseInternalId}.{collectionInternalId}.databases.storage';
|
||||
const METRIC_DATABASES_OPERATIONS_READS_VECTORSDB = 'vectorsdb.databases.operations.reads';
|
||||
const METRIC_DATABASE_ID_OPERATIONS_READS_VECTORSDB = 'vectorsdb.{databaseInternalId}.databases.operations.reads';
|
||||
const METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB = 'vectorsdb.databases.operations.writes';
|
||||
const METRIC_DATABASE_ID_OPERATIONS_WRITES_VECTORSDB = 'vectorsdb.{databaseInternalId}.databases.operations.writes';
|
||||
const METRIC_EMBEDDINGS_TEXT = 'embeddings.text';
|
||||
const METRIC_EMBEDDINGS_MODEL_TEXT = 'embeddings.text.{embeddingModel}';
|
||||
const METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR = 'embeddings.text.totalErrors';
|
||||
const METRIC_EMBEDDINGS_MODEL_TEXT_TOTAL_ERROR = 'embeddings.text.{embeddingModel}.totalErrors';
|
||||
const METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION = 'embeddings.text.totalDuration';
|
||||
const METRIC_EMBEDDINGS_MODEL_TEXT_TOTAL_DURATION = 'embeddings.text.{embeddingModel}.totalDuration';
|
||||
const METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS = 'embeddings.text.totalTokens';
|
||||
const METRIC_EMBEDDINGS_MODEL_TEXT_TOTAL_TOKENS = 'embeddings.text.{embeddingModel}.totalTokens';
|
||||
|
||||
const METRIC_BUCKETS = 'buckets';
|
||||
const METRIC_FILES = 'files';
|
||||
const METRIC_FILES_STORAGE = 'files.storage';
|
||||
@@ -380,6 +420,7 @@ const RESOURCE_TYPE_SUBSCRIBERS = 'subscribers';
|
||||
const RESOURCE_TYPE_MESSAGES = 'messages';
|
||||
const RESOURCE_TYPE_EXECUTIONS = 'executions';
|
||||
const RESOURCE_TYPE_VCS = 'vcs';
|
||||
const RESOURCE_TYPE_EMBEDDINGS_TEXT = 'embeddingsText';
|
||||
|
||||
// Resource types for Tokens
|
||||
const TOKENS_RESOURCE_TYPE_FILES = 'files';
|
||||
@@ -401,3 +442,16 @@ const CACHE_RECONNECT_RETRY_DELAY = 1000;
|
||||
|
||||
// Project status
|
||||
const PROJECT_STATUS_ACTIVE = 'active';
|
||||
|
||||
// Database types
|
||||
const DATABASE_TYPE_LEGACY = 'legacy';
|
||||
const DATABASE_TYPE_TABLESDB = 'tablesdb';
|
||||
const DATABASE_TYPE_DOCUMENTSDB = 'documentsdb';
|
||||
const DATABASE_TYPE_VECTORSDB = 'vectorsdb';
|
||||
|
||||
// CSV import/export allowed database types
|
||||
const CSV_ALLOWED_DATABASE_TYPES = [
|
||||
DATABASE_TYPE_LEGACY,
|
||||
DATABASE_TYPE_TABLESDB,
|
||||
DATABASE_TYPE_VECTORSDB
|
||||
];
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Database\Validator\Structure;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Validator\IP;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\URL;
|
||||
|
||||
+23
-1
@@ -22,6 +22,7 @@ use Appwrite\Utopia\Response\Model\AttributeLine;
|
||||
use Appwrite\Utopia\Response\Model\AttributeList;
|
||||
use Appwrite\Utopia\Response\Model\AttributeLongtext;
|
||||
use Appwrite\Utopia\Response\Model\AttributeMediumtext;
|
||||
use Appwrite\Utopia\Response\Model\AttributeObject;
|
||||
use Appwrite\Utopia\Response\Model\AttributePoint;
|
||||
use Appwrite\Utopia\Response\Model\AttributePolygon;
|
||||
use Appwrite\Utopia\Response\Model\AttributeRelationship;
|
||||
@@ -29,6 +30,7 @@ use Appwrite\Utopia\Response\Model\AttributeString;
|
||||
use Appwrite\Utopia\Response\Model\AttributeText;
|
||||
use Appwrite\Utopia\Response\Model\AttributeURL;
|
||||
use Appwrite\Utopia\Response\Model\AttributeVarchar;
|
||||
use Appwrite\Utopia\Response\Model\AttributeVector;
|
||||
use Appwrite\Utopia\Response\Model\AuthProvider;
|
||||
use Appwrite\Utopia\Response\Model\BaseList;
|
||||
use Appwrite\Utopia\Response\Model\Branch;
|
||||
@@ -65,6 +67,7 @@ use Appwrite\Utopia\Response\Model\DetectionRuntime;
|
||||
use Appwrite\Utopia\Response\Model\DetectionVariable;
|
||||
use Appwrite\Utopia\Response\Model\DevKey;
|
||||
use Appwrite\Utopia\Response\Model\Document as ModelDocument;
|
||||
use Appwrite\Utopia\Response\Model\Embedding;
|
||||
use Appwrite\Utopia\Response\Model\Error;
|
||||
use Appwrite\Utopia\Response\Model\ErrorDev;
|
||||
use Appwrite\Utopia\Response\Model\Execution;
|
||||
@@ -136,6 +139,8 @@ use Appwrite\Utopia\Response\Model\UsageBuckets;
|
||||
use Appwrite\Utopia\Response\Model\UsageCollection;
|
||||
use Appwrite\Utopia\Response\Model\UsageDatabase;
|
||||
use Appwrite\Utopia\Response\Model\UsageDatabases;
|
||||
use Appwrite\Utopia\Response\Model\UsageDocumentsDB;
|
||||
use Appwrite\Utopia\Response\Model\UsageDocumentsDBs;
|
||||
use Appwrite\Utopia\Response\Model\UsageFunction;
|
||||
use Appwrite\Utopia\Response\Model\UsageFunctions;
|
||||
use Appwrite\Utopia\Response\Model\UsageProject;
|
||||
@@ -144,9 +149,12 @@ use Appwrite\Utopia\Response\Model\UsageSites;
|
||||
use Appwrite\Utopia\Response\Model\UsageStorage;
|
||||
use Appwrite\Utopia\Response\Model\UsageTable;
|
||||
use Appwrite\Utopia\Response\Model\UsageUsers;
|
||||
use Appwrite\Utopia\Response\Model\UsageVectorsDB;
|
||||
use Appwrite\Utopia\Response\Model\UsageVectorsDBs;
|
||||
use Appwrite\Utopia\Response\Model\User;
|
||||
use Appwrite\Utopia\Response\Model\Variable;
|
||||
use Appwrite\Utopia\Response\Model\VcsContent;
|
||||
use Appwrite\Utopia\Response\Model\VectorsDBCollection;
|
||||
use Appwrite\Utopia\Response\Model\Webhook;
|
||||
|
||||
// General
|
||||
@@ -185,7 +193,7 @@ Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, '
|
||||
Response::setModel(new BaseList('Deployments List', Response::MODEL_DEPLOYMENT_LIST, 'deployments', Response::MODEL_DEPLOYMENT));
|
||||
Response::setModel(new BaseList('Executions List', Response::MODEL_EXECUTION_LIST, 'executions', Response::MODEL_EXECUTION));
|
||||
Response::setModel(new BaseList('Projects List', Response::MODEL_PROJECT_LIST, 'projects', Response::MODEL_PROJECT, true, false));
|
||||
Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, false));
|
||||
Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, true));
|
||||
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));
|
||||
@@ -211,9 +219,12 @@ Response::setModel(new BaseList('Migrations List', Response::MODEL_MIGRATION_LIS
|
||||
Response::setModel(new BaseList('Migrations Firebase Projects List', Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', Response::MODEL_MIGRATION_FIREBASE_PROJECT));
|
||||
Response::setModel(new BaseList('Specifications List', Response::MODEL_SPECIFICATION_LIST, 'specifications', Response::MODEL_SPECIFICATION));
|
||||
Response::setModel(new BaseList('VCS Content List', Response::MODEL_VCS_CONTENT_LIST, 'contents', Response::MODEL_VCS_CONTENT));
|
||||
Response::setModel(new BaseList('VectorsDB Collections List', Response::MODEL_VECTORSDB_COLLECTION_LIST, 'collections', Response::MODEL_VECTORSDB_COLLECTION));
|
||||
Response::setModel(new BaseList('Embedding list', Response::MODEL_EMBEDDING_LIST, 'embeddings', Response::MODEL_EMBEDDING));
|
||||
|
||||
// Entities
|
||||
Response::setModel(new Database());
|
||||
Response::setModel(new Embedding());
|
||||
|
||||
// Collection API Models
|
||||
Response::setModel(new Collection());
|
||||
@@ -237,6 +248,17 @@ Response::setModel(new AttributeText());
|
||||
Response::setModel(new AttributeMediumtext());
|
||||
Response::setModel(new AttributeLongtext());
|
||||
|
||||
// DocumentsDB API Models
|
||||
Response::setModel(new UsageDocumentsDBs());
|
||||
Response::setModel(new UsageDocumentsDB());
|
||||
|
||||
// VectorsDB API Models
|
||||
Response::setModel(new VectorsDBCollection());
|
||||
Response::setModel(new AttributeObject());
|
||||
Response::setModel(new AttributeVector());
|
||||
Response::setModel(new UsageVectorsDBs());
|
||||
Response::setModel(new UsageVectorsDB());
|
||||
|
||||
// Table API Models
|
||||
Response::setModel(new Table());
|
||||
Response::setModel(new Column());
|
||||
|
||||
+52
-39
@@ -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) {
|
||||
@@ -76,7 +76,7 @@ $register->set('logger', function () {
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -160,7 +160,6 @@ $register->set('pools', function () {
|
||||
'pass' => System::getEnv('_APP_DB_PASS', ''),
|
||||
'path' => System::getEnv('_APP_DB_SCHEMA', ''),
|
||||
]);
|
||||
|
||||
$fallbackForRedis = 'redis_main=' . AppwriteURL::unparse([
|
||||
'scheme' => 'redis',
|
||||
'host' => System::getEnv('_APP_REDIS_HOST', 'redis'),
|
||||
@@ -169,6 +168,23 @@ $register->set('pools', function () {
|
||||
'pass' => System::getEnv('_APP_REDIS_PASS', ''),
|
||||
]);
|
||||
|
||||
$fallbackForDocumentsDB = 'db_main=' . AppwriteURL::unparse([
|
||||
'scheme' => System::getEnv('_APP_DB_ADAPTER_DOCUMENTSDB', 'mongodb'),
|
||||
'host' => System::getEnv('_APP_DB_HOST_DOCUMENTSDB', 'mongodb'),
|
||||
'port' => System::getEnv('_APP_DB_PORT_DOCUMENTSDB', '27017'),
|
||||
'user' => System::getEnv('_APP_DB_USER', ''),
|
||||
'pass' => System::getEnv('_APP_DB_PASS', ''),
|
||||
'path' => System::getEnv('_APP_DB_SCHEMA', ''),
|
||||
]);
|
||||
$fallbackForVectorsDB = 'db_main=' . AppwriteURL::unparse([
|
||||
'scheme' => System::getEnv('_APP_DB_ADAPTER_VECTORSDB', 'postgresql'),
|
||||
'host' => System::getEnv('_APP_DB_HOST_VECTORSDB', 'postgresql'),
|
||||
'port' => System::getEnv('_APP_DB_PORT_VECTORSDB', '5432'),
|
||||
'user' => System::getEnv('_APP_DB_USER', ''),
|
||||
'pass' => System::getEnv('_APP_DB_PASS', ''),
|
||||
'path' => System::getEnv('_APP_DB_SCHEMA', ''),
|
||||
]);
|
||||
|
||||
$connections = [
|
||||
'console' => [
|
||||
'type' => 'database',
|
||||
@@ -180,13 +196,25 @@ $register->set('pools', function () {
|
||||
'type' => 'database',
|
||||
'dsns' => $fallbackForDB,
|
||||
'multiple' => true,
|
||||
'schemes' => ['mariadb', 'mongodb', 'mysql', 'postgresql'],
|
||||
'schemes' => ['mongodb','mariadb', 'mysql','postgresql'],
|
||||
],
|
||||
'documentsdb' => [
|
||||
'type' => 'database',
|
||||
'dsns' => System::getEnv('_APP_CONNECTIONS_DATABASE_DOCUMENTSDB', $fallbackForDocumentsDB),
|
||||
'multiple' => true,
|
||||
'schemes' => ['mongodb'],
|
||||
],
|
||||
'vectorsdb' => [
|
||||
'type' => 'database',
|
||||
'dsns' => System::getEnv('_APP_CONNECTIONS_DATABASE_VECTORSDB', $fallbackForVectorsDB),
|
||||
'multiple' => true,
|
||||
'schemes' => ['postgresql'],
|
||||
],
|
||||
'logs' => [
|
||||
'type' => 'database',
|
||||
'dsns' => System::getEnv('_APP_CONNECTIONS_DB_LOGS', $fallbackForDB),
|
||||
'multiple' => false,
|
||||
'schemes' => ['mariadb', 'mongodb', 'mysql', 'postgresql'],
|
||||
'schemes' => ['mongodb','mariadb', 'mysql','postgresql'],
|
||||
],
|
||||
'publisher' => [
|
||||
'type' => 'publisher',
|
||||
@@ -214,8 +242,8 @@ $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';
|
||||
|
||||
@@ -280,7 +308,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();
|
||||
@@ -405,35 +433,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');
|
||||
|
||||
+238
-20
@@ -32,6 +32,8 @@ use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Executor\Executor;
|
||||
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
|
||||
use Utopia\Agents\Adapters\Ollama;
|
||||
use Utopia\Agents\Agent;
|
||||
use Utopia\Audit\Adapter\Database as AdapterDatabase;
|
||||
use Utopia\Audit\Audit;
|
||||
use Utopia\Auth\Hashes\Argon2;
|
||||
@@ -430,8 +432,10 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
|
||||
$jwtUserId = $payload['userId'] ?? '';
|
||||
if (! empty($jwtUserId)) {
|
||||
if ($mode === APP_MODE_ADMIN) {
|
||||
/** @var User $user */
|
||||
$user = $dbForPlatform->getDocument('users', $jwtUserId);
|
||||
} else {
|
||||
/** @var User $user */
|
||||
$user = $dbForProject->getDocument('users', $jwtUserId);
|
||||
}
|
||||
}
|
||||
@@ -451,6 +455,7 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
|
||||
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
|
||||
}
|
||||
|
||||
/** @var User $accountKeyUser */
|
||||
$accountKeyUser = $dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId));
|
||||
if (! $accountKeyUser->isEmpty()) {
|
||||
$key = $accountKeyUser->find(
|
||||
@@ -470,13 +475,38 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
|
||||
}
|
||||
}
|
||||
|
||||
// Impersonation: if current user has impersonator capability and headers are set, act as another user
|
||||
$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 = (APP_MODE_ADMIN === $mode || $project->getId() === 'console') ? $dbForPlatform : $dbForProject;
|
||||
$targetUser = null;
|
||||
if (!empty($impersonateUserId)) {
|
||||
$targetUser = $userDb->getAuthorization()->skip(fn () => $userDb->getDocument('users', $impersonateUserId));
|
||||
} elseif (!empty($impersonateEmail)) {
|
||||
$targetUser = $userDb->getAuthorization()->skip(fn () => $userDb->findOne('users', [Query::equal('email', [\strtolower($impersonateEmail)])]));
|
||||
} elseif (!empty($impersonatePhone)) {
|
||||
$targetUser = $userDb->getAuthorization()->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));
|
||||
}
|
||||
}
|
||||
|
||||
$dbForProject->setMetadata('user', $user->getId());
|
||||
$dbForPlatform->setMetadata('user', $user->getId());
|
||||
|
||||
return $user;
|
||||
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']);
|
||||
|
||||
Http::setResource('project', function ($dbForPlatform, $request, $console, $authorization) {
|
||||
Http::setResource('project', function ($dbForPlatform, $request, $console, $authorization, Http $utopia) {
|
||||
/** @var Appwrite\Utopia\Request $request */
|
||||
/** @var Utopia\Database\Database $dbForPlatform */
|
||||
/** @var Utopia\Database\Document $console */
|
||||
@@ -486,6 +516,20 @@ Http::setResource('project', function ($dbForPlatform, $request, $console, $auth
|
||||
$projectId = $request->getHeader('x-appwrite-project', '');
|
||||
}
|
||||
|
||||
// Backwards compatibility for new services, originally project resources
|
||||
// These endpoints moved from /v1/projects/:projectId/<resource> to /v1/<resource>
|
||||
// When accessed via the old alias path, extract projectId from the URI
|
||||
$deprecatedProjectPathPrefix = '/v1/projects/';
|
||||
$route = $utopia->match($request);
|
||||
if (!empty($route)) {
|
||||
$isDeprecatedAlias = \str_starts_with($request->getURI(), $deprecatedProjectPathPrefix) &&
|
||||
!\str_starts_with($route->getPath(), $deprecatedProjectPathPrefix);
|
||||
|
||||
if ($isDeprecatedAlias) {
|
||||
$projectId = \explode('/', $request->getURI(), 5)[3] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($projectId) || $projectId === 'console') {
|
||||
return $console;
|
||||
}
|
||||
@@ -493,7 +537,7 @@ Http::setResource('project', function ($dbForPlatform, $request, $console, $auth
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
|
||||
|
||||
return $project;
|
||||
}, ['dbForPlatform', 'request', 'console', 'authorization']);
|
||||
}, ['dbForPlatform', 'request', 'console', 'authorization', 'utopia']);
|
||||
|
||||
Http::setResource('session', function (User $user, Store $store, Token $proofForToken) {
|
||||
if ($user->isEmpty()) {
|
||||
@@ -555,7 +599,7 @@ Http::setResource('authorization', function () {
|
||||
return new Authorization();
|
||||
}, []);
|
||||
|
||||
Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization) {
|
||||
Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization, Request $request) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
}
|
||||
@@ -652,7 +696,7 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
$cacheKey = \sprintf(
|
||||
'%s-cache-%s:%s:%s:project:%s:functions:events',
|
||||
$dbForProject->getCacheName(),
|
||||
$hostname ?? '',
|
||||
$hostname,
|
||||
$dbForProject->getNamespace(),
|
||||
$dbForProject->getTenant(),
|
||||
$project->getId()
|
||||
@@ -661,7 +705,31 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
$dbForProject->getCache()->purge($cacheKey);
|
||||
};
|
||||
|
||||
$usageDatabaseListener = function (string $event, Document $document, UsageContext $usage) {
|
||||
/**
|
||||
* Prefix metrics with database type when applicable.
|
||||
* Avoids prefixing for legacy and tablesdb types to preserve historical metrics.
|
||||
*/
|
||||
$getDatabaseTypePrefixedMetric = function (string $databaseType, string $metric): string {
|
||||
if (
|
||||
$databaseType === '' ||
|
||||
$databaseType === DATABASE_TYPE_LEGACY ||
|
||||
$databaseType === DATABASE_TYPE_TABLESDB
|
||||
) {
|
||||
return $metric;
|
||||
}
|
||||
|
||||
return $databaseType . '.' . $metric;
|
||||
};
|
||||
|
||||
// Determine database type from request path, similar to api.php
|
||||
$path = $request->getURI();
|
||||
$databaseType = match (true) {
|
||||
str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
|
||||
str_contains($path, '/vectorsdb') => DATABASE_TYPE_VECTORSDB,
|
||||
default => '',
|
||||
};
|
||||
|
||||
$usageDatabaseListener = function (string $event, Document $document, UsageContext $usage) use ($getDatabaseTypePrefixedMetric, $databaseType) {
|
||||
$value = 1;
|
||||
|
||||
switch ($event) {
|
||||
@@ -693,7 +761,8 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
$usage->addMetric(METRIC_SESSIONS, $value); // per project
|
||||
break;
|
||||
case $document->getCollection() === 'databases': // databases
|
||||
$usage->addMetric(METRIC_DATABASES, $value); // per project
|
||||
$metric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASES);
|
||||
$usage->addMetric($metric, $value); // per project
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$usage->addReduce($document);
|
||||
@@ -702,9 +771,11 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
case str_starts_with($document->getCollection(), 'database_') && ! str_contains($document->getCollection(), 'collection'): // collections
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$collectionMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_COLLECTIONS);
|
||||
$databaseIdCollectionMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASE_ID_COLLECTIONS);
|
||||
$usage
|
||||
->addMetric(METRIC_COLLECTIONS, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value);
|
||||
->addMetric($collectionMetric, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdCollectionMetric), $value);
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$usage->addReduce($document);
|
||||
@@ -714,10 +785,13 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$collectionInternalId = $parts[3] ?? 0;
|
||||
$documentsMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DOCUMENTS);
|
||||
$databaseIdDocumentsMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASE_ID_DOCUMENTS);
|
||||
$databaseIdCollectionIdDocumentsMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS);
|
||||
$usage
|
||||
->addMetric(METRIC_DOCUMENTS, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), $value) // per database
|
||||
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), $value); // per collection
|
||||
->addMetric($documentsMetric, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
|
||||
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
|
||||
break;
|
||||
case $document->getCollection() === 'buckets': // buckets
|
||||
$usage->addMetric(METRIC_BUCKETS, $value); // per project
|
||||
@@ -792,7 +866,7 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
->on(Database::EVENT_DOCUMENT_DELETE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database));
|
||||
|
||||
return $database;
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization']);
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization', 'request']);
|
||||
|
||||
Http::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
|
||||
@@ -813,6 +887,138 @@ Http::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authori
|
||||
return $database;
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
|
||||
|
||||
return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization): Database {
|
||||
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
|
||||
$databaseType = $database->getAttribute('type', '');
|
||||
|
||||
try {
|
||||
$databaseDSN = new DSN($databaseDSN);
|
||||
} catch (\InvalidArgumentException) {
|
||||
// for old databases migrated through patch script
|
||||
// databaseDSN determines the adapter
|
||||
$databaseDSN = new DSN('mysql://'.$databaseDSN);
|
||||
}
|
||||
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'));
|
||||
}
|
||||
|
||||
$pool = $pools->get($databaseDSN->getHost());
|
||||
|
||||
$adapter = new DatabasePool($pool);
|
||||
$database = new Database($adapter, $cache);
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setMetadata('host', \gethostname())
|
||||
->setMetadata('project', $project->getId())
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
|
||||
// inside pools authorization needs to be set first
|
||||
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant((int)$project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
$timeout = \intval($request->getHeader('x-appwrite-timeout'));
|
||||
if (!empty($timeout) && Http::isDevelopment()) {
|
||||
$database->setTimeout($timeout);
|
||||
}
|
||||
|
||||
// Register database event listeners for usage stats collection
|
||||
$documentsMetric = METRIC_DOCUMENTS;
|
||||
$databaseIdDocumentsMetric = METRIC_DATABASE_ID_DOCUMENTS;
|
||||
$databaseIdCollectionIdDocumentsMetric = METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS;
|
||||
if ($databaseType !== DATABASE_TYPE_LEGACY && $databaseType !== DATABASE_TYPE_TABLESDB) {
|
||||
$documentsMetric = $databaseType. '.' .$documentsMetric;
|
||||
$databaseIdDocumentsMetric = $databaseType. '.' .$databaseIdDocumentsMetric;
|
||||
$databaseIdCollectionIdDocumentsMetric = $databaseType . '.' .$databaseIdCollectionIdDocumentsMetric;
|
||||
}
|
||||
$database
|
||||
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) {
|
||||
$value = 1;
|
||||
|
||||
if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) {
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$collectionInternalId = $parts[3] ?? 0;
|
||||
$usage
|
||||
->addMetric($documentsMetric, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
|
||||
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
|
||||
}
|
||||
})
|
||||
->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) {
|
||||
$value = -1;
|
||||
|
||||
if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) {
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$collectionInternalId = $parts[3] ?? 0;
|
||||
$usage
|
||||
->addMetric($documentsMetric, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
|
||||
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
|
||||
}
|
||||
})
|
||||
->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) {
|
||||
$value = $document->getAttribute('modified', 0);
|
||||
|
||||
if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) {
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$collectionInternalId = $parts[3] ?? 0;
|
||||
$usage
|
||||
->addMetric($documentsMetric, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
|
||||
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
|
||||
}
|
||||
})
|
||||
->on(Database::EVENT_DOCUMENTS_DELETE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) {
|
||||
$value = -1 * $document->getAttribute('modified', 0);
|
||||
|
||||
if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) {
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$collectionInternalId = $parts[3] ?? 0;
|
||||
$usage
|
||||
->addMetric($documentsMetric, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
|
||||
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
|
||||
}
|
||||
})
|
||||
->on(Database::EVENT_DOCUMENTS_UPSERT, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) {
|
||||
$value = $document->getAttribute('created', 0);
|
||||
|
||||
if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) {
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$collectionInternalId = $parts[3] ?? 0;
|
||||
$usage
|
||||
->addMetric($documentsMetric, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
|
||||
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
|
||||
}
|
||||
});
|
||||
|
||||
return $database;
|
||||
};
|
||||
|
||||
}, ['pools','cache','project','request','usage','authorization']);
|
||||
|
||||
Http::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) {
|
||||
$databases = [];
|
||||
|
||||
@@ -1072,16 +1278,21 @@ function getDevice(string $root, string $connection = ''): Device
|
||||
}
|
||||
}
|
||||
|
||||
Http::setResource('mode', function ($request) {
|
||||
/** @var Appwrite\Utopia\Request $request */
|
||||
|
||||
Http::setResource('mode', function (Request $request, Document $project) {
|
||||
/**
|
||||
* Defines the mode for the request:
|
||||
* - 'default' => Requests for Client and Server Side
|
||||
* - 'admin' => Request from the Console on non-console projects
|
||||
*/
|
||||
return $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT));
|
||||
}, ['request']);
|
||||
$mode = $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT));
|
||||
|
||||
$projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', ''));
|
||||
if (!empty($projectId) && $project->getId() !== $projectId) {
|
||||
$mode = APP_MODE_ADMIN;
|
||||
}
|
||||
|
||||
return $mode;
|
||||
}, ['request', 'project']);
|
||||
|
||||
Http::setResource('geodb', function ($register) {
|
||||
/** @var Utopia\Registry\Registry $register */
|
||||
@@ -1441,9 +1652,9 @@ Http::setResource('resourceToken', function ($project, $dbForProject, $request,
|
||||
return new Document([]);
|
||||
}, ['project', 'dbForProject', 'request', 'authorization']);
|
||||
|
||||
Http::setResource('transactionState', function (Database $dbForProject, Authorization $authorization) {
|
||||
return new TransactionState($dbForProject, $authorization);
|
||||
}, ['dbForProject', 'authorization']);
|
||||
Http::setResource('transactionState', function (Database $dbForProject, Authorization $authorization, callable $getDatabasesDB) {
|
||||
return new TransactionState($dbForProject, $authorization, $getDatabasesDB);
|
||||
}, ['dbForProject', 'authorization', 'getDatabasesDB']);
|
||||
|
||||
Http::setResource('executionsRetentionCount', function (Document $project, array $plan) {
|
||||
if ($project->getId() === 'console' || empty($plan)) {
|
||||
@@ -1452,3 +1663,10 @@ Http::setResource('executionsRetentionCount', function (Document $project, array
|
||||
|
||||
return (int) ($plan['executionsRetentionCount'] ?? 100);
|
||||
}, ['project', 'plan']);
|
||||
|
||||
Http::setResource('embeddingAgent', function ($register) {
|
||||
$adapter = new Ollama();
|
||||
$adapter->setEndpoint(System::getEnv('_APP_EMBEDDING_ENDPOINT', 'http://ollama:11434/api/embed'));
|
||||
$adapter->setTimeout((int) System::getEnv('_APP_EMBEDDING_TIMEOUT', '30000'));
|
||||
return new Agent($adapter);
|
||||
}, ['register']);
|
||||
|
||||
+31
-13
@@ -237,7 +237,6 @@ if (!function_exists('getTelemetry')) {
|
||||
if (!function_exists('triggerStats')) {
|
||||
function triggerStats(array $event, string $projectId): void
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,14 +319,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 +356,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');
|
||||
@@ -384,6 +383,22 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
|
||||
}
|
||||
});
|
||||
|
||||
function cloudRealtimeLogConnectionHostnames(Http $app, Document $project, Request $request): void
|
||||
{
|
||||
try {
|
||||
/** @var array<int, string> $allowed */
|
||||
$allowed = $app->getResource('allowedHostnames');
|
||||
Console::info(sprintf(
|
||||
'[Realtime] project=%s origin=%s allowedHostnames=%s',
|
||||
$project->getId(),
|
||||
$request->getOrigin(),
|
||||
json_encode(array_values($allowed))
|
||||
));
|
||||
} catch (Throwable $e) {
|
||||
Console::error('[Realtime] allowedHostnames log failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime) {
|
||||
Console::success('Worker ' . $workerId . ' started successfully');
|
||||
|
||||
@@ -396,7 +411,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
$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.
|
||||
*/
|
||||
@@ -518,7 +533,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
$project = $consoleDatabase->getAuthorization()->skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
|
||||
$database = getProjectDB($project);
|
||||
|
||||
/** @var Appwrite\Utopia\Database\Documents\User $user */
|
||||
/** @var User $user */
|
||||
$user = $database->getDocument('users', $userId);
|
||||
|
||||
$roles = $user->getRoles($database->getAuthorization());
|
||||
@@ -642,10 +657,14 @@ $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 */
|
||||
$logUser = $user;
|
||||
|
||||
if (
|
||||
array_key_exists('realtime', $project->getAttribute('apis', []))
|
||||
&& !$project->getAttribute('apis', [])['realtime']
|
||||
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
&& !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
@@ -656,10 +675,6 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Project is not accessible in this region. Please make sure you are using the correct endpoint');
|
||||
}
|
||||
|
||||
$timelimit = $app->getResource('timelimit');
|
||||
$user = $app->getResource('user'); /** @var User $user */
|
||||
$logUser = $user;
|
||||
|
||||
/*
|
||||
* Abuse Check
|
||||
*
|
||||
@@ -689,6 +704,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
*/
|
||||
$origin = $request->getOrigin();
|
||||
$originValidator = $app->getResource('originValidator');
|
||||
|
||||
|
||||
cloudRealtimeLogConnectionHostnames($app, $project, $request);
|
||||
|
||||
if (!empty($origin) && !$originValidator->isValid($origin) && $project->getId() !== 'console') {
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
|
||||
@@ -798,7 +816,7 @@ $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) {
|
||||
$project = null;
|
||||
$authorization = null;
|
||||
|
||||
@@ -810,7 +828,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();
|
||||
|
||||
@@ -13,7 +13,7 @@ $organization = $this->getParam('organization', '');
|
||||
$image = $this->getParam('image', '');
|
||||
$enableAssistant = $this->getParam('enableAssistant', false);
|
||||
$dbService = $this->getParam('database', 'mongodb');
|
||||
$allowedDbServices = ['mariadb', 'mongodb', 'postgresql'];
|
||||
$allowedDbServices = ['mariadb', 'mongodb'];
|
||||
if (!\in_array($dbService, $allowedDbServices, true)) {
|
||||
$dbService = 'mongodb';
|
||||
}
|
||||
@@ -194,7 +194,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
appwrite-console:
|
||||
<<: *x-logging
|
||||
container_name: appwrite-console
|
||||
image: <?php echo $organization; ?>/console:7.6.4
|
||||
image: <?php echo $organization; ?>/console:7.8.26
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
|
||||
@@ -9,10 +9,11 @@ $defaultAppDomain = ($defaultAppDomain === 'traefik') ? 'localhost' : $defaultAp
|
||||
$defaultEmailCertificates = $defaultEmailCertificates ?? '';
|
||||
$defaultDatabase = $vars['_APP_DB_ADAPTER']['default'] ?? 'mongodb';
|
||||
$lockedDatabase = $isUpgrade && empty($lockedDatabase) ? $defaultDatabase : $lockedDatabase;
|
||||
$enabledDatabases = $enabledDatabases ?? ['mongodb', 'mariadb', 'postgresql'];
|
||||
$isLocalInstall = $isLocalInstall ?? false;
|
||||
|
||||
|
||||
$cardStep = min(4, $step);
|
||||
$cardStep = ($step === 5) ? 4 : $step;
|
||||
$stepFile = __DIR__ . "/installer/templates/steps/step-{$cardStep}.phtml";
|
||||
if (!is_file($stepFile)) {
|
||||
$stepFile = __DIR__ . "/installer/templates/steps/step-1.phtml";
|
||||
@@ -64,6 +65,7 @@ $installerVersion = @filemtime(__DIR__ . '/installer/js/installer.js') ?: time()
|
||||
data-default-secret-key=""
|
||||
data-default-assistant-openai-key=""
|
||||
data-default-database="<?php echo htmlspecialchars((string) $defaultDatabase, ENT_QUOTES, 'UTF-8'); ?>"
|
||||
data-enabled-databases="<?php echo htmlspecialchars(json_encode(array_values($enabledDatabases)), ENT_QUOTES, 'UTF-8'); ?>"
|
||||
<?php if ($isLocalInstall) { ?>
|
||||
data-dev-mode="true"
|
||||
<?php } ?>
|
||||
|
||||
@@ -11,9 +11,13 @@
|
||||
--neutral-300: rgba(173, 173, 176, 1);
|
||||
--neutral-400: rgba(151, 151, 155, 1);
|
||||
--neutral-500: rgba(129, 129, 134, 1);
|
||||
--neutral-600: rgba(108, 108, 113, 1);
|
||||
--neutral-700: rgba(86, 86, 92, 1);
|
||||
--neutral-750: rgba(65, 65, 70, 1);
|
||||
--neutral-800: rgba(45, 45, 49, 1);
|
||||
--neutral-850: rgba(29, 29, 33, 1);
|
||||
--neutral-900: rgba(25, 25, 28, 1);
|
||||
--neutral-250: rgba(195, 195, 198, 1);
|
||||
|
||||
/* Warning colors */
|
||||
--web-orange-200: rgba(255, 213, 194, 1);
|
||||
@@ -172,6 +176,44 @@
|
||||
--fgColor-neutral-weak: var(--fgcolor-neutral-weak);
|
||||
--fgColor-accent: var(--fgcolor-accent);
|
||||
--fgColor-on-accent: var(--fgcolor-on-accent);
|
||||
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bgcolor-neutral-default: var(--neutral-900);
|
||||
--bgcolor-neutral-primary: var(--neutral-850);
|
||||
--bgcolor-neutral-secondary: var(--neutral-800);
|
||||
--bgcolor-neutral-tertiary: var(--neutral-800);
|
||||
--bgcolor-neutral-invert-weaker: var(--neutral-400);
|
||||
--bgcolor-neutral-invert-weak: var(--neutral-300);
|
||||
--bgcolor-success-weak: rgba(16, 185, 129, 0.12);
|
||||
--bgcolor-warning-weaker: rgba(254, 124, 67, 0.08);
|
||||
--bgcolor-warning-weak: rgba(254, 124, 67, 0.12);
|
||||
--bgcolor-error-weaker: rgba(255, 69, 58, 0.08);
|
||||
|
||||
--fgcolor-neutral-primary: var(--neutral-50);
|
||||
--fgcolor-neutral-secondary: var(--neutral-250);
|
||||
--fgcolor-neutral-tertiary: var(--neutral-500);
|
||||
--fgcolor-neutral-weak: var(--neutral-600);
|
||||
--fgcolor-on-accent: var(--neutral-0);
|
||||
--fgcolor-on-invert: var(--neutral-900);
|
||||
--fgcolor-on-success-weak: rgba(52, 211, 153, 1);
|
||||
--fgcolor-warning: var(--web-orange-500);
|
||||
--fgcolor-on-warning-weak: var(--web-orange-500);
|
||||
--fgcolor-error: var(--web-red-500);
|
||||
--fgcolor-on-error: var(--neutral-0);
|
||||
|
||||
--border-neutral: var(--neutral-800);
|
||||
--border-neutral-strong: var(--neutral-750);
|
||||
--border-neutral-stronger: var(--neutral-600);
|
||||
--border-focus: var(--neutral-600);
|
||||
|
||||
--overlay-neutral-hover: rgba(255, 255, 255, 0.04);
|
||||
--overlay-neutral-pressed: rgba(255, 255, 255, 0.08);
|
||||
--overlay-scrim: rgba(0, 0, 0, 0.48);
|
||||
}
|
||||
}
|
||||
|
||||
.installer-toast-stack {
|
||||
@@ -436,6 +478,10 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.installer-page[data-upgrade='true'] .installer-step {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.action-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -649,6 +695,19 @@ body {
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.install-counter {
|
||||
margin-left: auto;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
color: var(--fgcolor-neutral-secondary);
|
||||
}
|
||||
|
||||
.install-row[data-status='in-progress'] .install-counter:not(:empty) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.install-row-toggle {
|
||||
margin-left: auto;
|
||||
width: 32px;
|
||||
@@ -855,6 +914,17 @@ body {
|
||||
gap: var(--gap-m);
|
||||
}
|
||||
|
||||
.install-global-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--gap-m);
|
||||
padding: var(--space-4) 0;
|
||||
}
|
||||
|
||||
.install-global-actions.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.install-error-details .button {
|
||||
align-self: center;
|
||||
margin-top: 0;
|
||||
@@ -1354,6 +1424,7 @@ body {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 0;
|
||||
color: var(--fgcolor-neutral-weak);
|
||||
}
|
||||
|
||||
.step-indicator svg {
|
||||
@@ -1741,3 +1812,92 @@ body {
|
||||
gap: var(--gap-s);
|
||||
}
|
||||
}
|
||||
|
||||
.migration-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--gap-l);
|
||||
padding: var(--space-6);
|
||||
background: var(--bgcolor-neutral-default);
|
||||
border-radius: var(--border-radius-m);
|
||||
outline: var(--border-width-s) solid var(--border-neutral);
|
||||
outline-offset: calc(var(--border-width-s) * -1);
|
||||
cursor: pointer;
|
||||
transition: outline-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.migration-option:hover {
|
||||
outline-color: var(--border-neutral-stronger);
|
||||
}
|
||||
|
||||
.migration-option-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.migration-switch {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.migration-switch-track {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 32px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
background: var(--bgcolor-neutral-invert-weaker);
|
||||
transition: background 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.migration-switch-thumb {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--bgcolor-neutral-primary);
|
||||
transition: transform 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
#run-migration:checked ~ .migration-switch-track {
|
||||
background: var(--bgcolor-neutral-invert-weak);
|
||||
}
|
||||
|
||||
#run-migration:checked ~ .migration-switch-track .migration-switch-thumb {
|
||||
transform: translateX(12px);
|
||||
}
|
||||
|
||||
#run-migration:focus-visible ~ .migration-switch-track {
|
||||
box-shadow: 0 0 0 var(--border-width-l) var(--border-focus);
|
||||
}
|
||||
|
||||
.migration-hint {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--gap-s);
|
||||
padding: 0 var(--space-2);
|
||||
}
|
||||
|
||||
.migration-hint-icon {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--fgcolor-neutral-tertiary);
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.migration-hint-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.migration-code {
|
||||
padding: 1px 4px;
|
||||
border-radius: var(--border-radius-xs, 4px);
|
||||
background: var(--bgcolor-neutral-secondary);
|
||||
font-family: monospace;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<svg width="131" height="25" viewBox="0 0 131 25" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M38.2643 19.5087C40.4069 19.5087 41.49 18.3947 41.9609 17.6362H42.1728C42.267 18.4421 42.832 19.2006 43.9386 19.2006H46.0341V16.8304H45.4926C45.1159 16.8304 44.9275 16.617 44.9275 16.2852V6.78055H42.1492V8.29751H41.9373C41.3958 7.53903 40.2656 6.47242 38.1937 6.47242C34.8974 6.47242 32.4487 9.2219 32.4487 12.9906C32.4487 16.7593 34.9445 19.5087 38.2643 19.5087ZM38.7588 16.8067C36.8045 16.8067 35.2741 15.3608 35.2741 13.0143C35.2741 10.7151 36.7574 9.15079 38.7352 9.15079C40.6188 9.15079 42.1963 10.5492 42.1963 13.0143C42.1963 15.1238 40.8543 16.8067 38.7588 16.8067Z" fill="#19191D"/>
|
||||
<path d="M47.6745 24.0166H50.4528V17.6362H50.6647C51.1826 18.3947 52.2893 19.5087 54.4789 19.5087C57.7752 19.5087 60.1768 16.7118 60.1768 12.9906C60.1768 9.2456 57.6104 6.47242 54.2906 6.47242C52.1715 6.47242 51.1356 7.63384 50.6411 8.2738H50.4292V6.78055H47.6745V24.0166ZM53.8903 16.8778C51.9832 16.8778 50.4057 15.4556 50.4057 12.9906C50.4057 10.8811 51.7477 9.10339 53.8432 9.10339C55.7974 9.10339 57.3279 10.644 57.3279 12.9906C57.3279 15.2897 55.8445 16.8778 53.8903 16.8778Z" fill="#19191D"/>
|
||||
<path d="M61.6104 24.0166H64.3887V17.6362H64.6006C65.1186 18.3947 66.2252 19.5087 68.4149 19.5087C71.7112 19.5087 73.8839 16.7118 73.8839 12.9906C73.8839 9.2456 71.5464 6.47242 68.2265 6.47242C66.1075 6.47242 65.0715 7.63384 64.5771 8.2738H64.3652V6.78055H61.6104V24.0166ZM67.8263 16.8778C65.9191 16.8778 64.3416 15.4556 64.3416 12.9906C64.3416 10.8811 65.6837 9.10339 67.7792 9.10339C69.7334 9.10339 71.2638 10.644 71.2638 12.9906C71.2638 15.2897 69.7805 16.8778 67.8263 16.8778Z" fill="#19191D"/>
|
||||
<path d="M77.5565 19.489H81.4885L83.7252 9.74733H83.8665L86.1033 19.489H90.0117L93.1415 7.06896H90.3414L88.1046 16.8343H87.8927L85.6559 7.06896H81.9594L79.6991 16.8343H79.4872L77.2739 7.06896H74.3073L77.5565 19.489Z" fill="#19191D"/>
|
||||
<path d="M94.549 19.489H97.3273V13.3501C97.3273 11.0036 98.4104 9.55771 100.435 9.55771H101.66V6.76083H100.741C99.1638 6.76083 97.963 7.85114 97.4921 8.89405H97.3038V7.06896H94.549V19.489Z" fill="#19191D"/>
|
||||
<path d="M115.447 19.489H117.613V17.0003H115.47C114.623 17.0003 114.27 16.621 114.27 15.744V9.53401H117.754V7.06896H114.27V3.58472H111.633V7.06896H109.325V9.53401H111.468V15.7677C111.468 18.3987 113.045 19.489 115.447 19.489Z" fill="#19191D"/>
|
||||
<path d="M125.067 19.5087C127.633 19.5087 129.893 18.2288 130.694 15.6452L128.151 15.029C127.704 16.4037 126.409 17.1148 125.043 17.1148C123.018 17.1148 121.676 15.7875 121.653 13.7016H131V12.9195C131 9.2219 128.716 6.47242 124.949 6.47242C121.629 6.47242 118.78 9.10339 118.78 13.0143C118.78 16.8067 121.3 19.5087 125.067 19.5087ZM121.676 11.6632C121.841 10.17 123.183 8.91377 124.949 8.91377C126.644 8.91377 128.033 9.98037 128.175 11.6632H121.676Z" fill="#19191D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M108.09 19.489H105.312V9.53401H103.145V7.06896H108.09V19.489Z" fill="#19191D"/>
|
||||
<path d="M106.494 5.34533C107.507 5.34533 108.26 4.58686 108.26 3.59136C108.26 2.61956 107.507 1.86108 106.494 1.86108C105.482 1.86108 104.729 2.61956 104.729 3.59136C104.729 4.58686 105.482 5.34533 106.494 5.34533Z" fill="#19191D"/>
|
||||
<path d="M38.2643 19.5087C40.4069 19.5087 41.49 18.3947 41.9609 17.6362H42.1728C42.267 18.4421 42.832 19.2006 43.9386 19.2006H46.0341V16.8304H45.4926C45.1159 16.8304 44.9275 16.617 44.9275 16.2852V6.78055H42.1492V8.29751H41.9373C41.3958 7.53903 40.2656 6.47242 38.1937 6.47242C34.8974 6.47242 32.4487 9.2219 32.4487 12.9906C32.4487 16.7593 34.9445 19.5087 38.2643 19.5087ZM38.7588 16.8067C36.8045 16.8067 35.2741 15.3608 35.2741 13.0143C35.2741 10.7151 36.7574 9.15079 38.7352 9.15079C40.6188 9.15079 42.1963 10.5492 42.1963 13.0143C42.1963 15.1238 40.8543 16.8067 38.7588 16.8067Z" fill="currentColor"/>
|
||||
<path d="M47.6745 24.0166H50.4528V17.6362H50.6647C51.1826 18.3947 52.2893 19.5087 54.4789 19.5087C57.7752 19.5087 60.1768 16.7118 60.1768 12.9906C60.1768 9.2456 57.6104 6.47242 54.2906 6.47242C52.1715 6.47242 51.1356 7.63384 50.6411 8.2738H50.4292V6.78055H47.6745V24.0166ZM53.8903 16.8778C51.9832 16.8778 50.4057 15.4556 50.4057 12.9906C50.4057 10.8811 51.7477 9.10339 53.8432 9.10339C55.7974 9.10339 57.3279 10.644 57.3279 12.9906C57.3279 15.2897 55.8445 16.8778 53.8903 16.8778Z" fill="currentColor"/>
|
||||
<path d="M61.6104 24.0166H64.3887V17.6362H64.6006C65.1186 18.3947 66.2252 19.5087 68.4149 19.5087C71.7112 19.5087 73.8839 16.7118 73.8839 12.9906C73.8839 9.2456 71.5464 6.47242 68.2265 6.47242C66.1075 6.47242 65.0715 7.63384 64.5771 8.2738H64.3652V6.78055H61.6104V24.0166ZM67.8263 16.8778C65.9191 16.8778 64.3416 15.4556 64.3416 12.9906C64.3416 10.8811 65.6837 9.10339 67.7792 9.10339C69.7334 9.10339 71.2638 10.644 71.2638 12.9906C71.2638 15.2897 69.7805 16.8778 67.8263 16.8778Z" fill="currentColor"/>
|
||||
<path d="M77.5565 19.489H81.4885L83.7252 9.74733H83.8665L86.1033 19.489H90.0117L93.1415 7.06896H90.3414L88.1046 16.8343H87.8927L85.6559 7.06896H81.9594L79.6991 16.8343H79.4872L77.2739 7.06896H74.3073L77.5565 19.489Z" fill="currentColor"/>
|
||||
<path d="M94.549 19.489H97.3273V13.3501C97.3273 11.0036 98.4104 9.55771 100.435 9.55771H101.66V6.76083H100.741C99.1638 6.76083 97.963 7.85114 97.4921 8.89405H97.3038V7.06896H94.549V19.489Z" fill="currentColor"/>
|
||||
<path d="M115.447 19.489H117.613V17.0003H115.47C114.623 17.0003 114.27 16.621 114.27 15.744V9.53401H117.754V7.06896H114.27V3.58472H111.633V7.06896H109.325V9.53401H111.468V15.7677C111.468 18.3987 113.045 19.489 115.447 19.489Z" fill="currentColor"/>
|
||||
<path d="M125.067 19.5087C127.633 19.5087 129.893 18.2288 130.694 15.6452L128.151 15.029C127.704 16.4037 126.409 17.1148 125.043 17.1148C123.018 17.1148 121.676 15.7875 121.653 13.7016H131V12.9195C131 9.2219 128.716 6.47242 124.949 6.47242C121.629 6.47242 118.78 9.10339 118.78 13.0143C118.78 16.8067 121.3 19.5087 125.067 19.5087ZM121.676 11.6632C121.841 10.17 123.183 8.91377 124.949 8.91377C126.644 8.91377 128.033 9.98037 128.175 11.6632H121.676Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M108.09 19.489H105.312V9.53401H103.145V7.06896H108.09V19.489Z" fill="currentColor"/>
|
||||
<path d="M106.494 5.34533C107.507 5.34533 108.26 4.58686 108.26 3.59136C108.26 2.61956 107.507 1.86108 106.494 1.86108C105.482 1.86108 104.729 2.61956 104.729 3.59136C104.729 4.58686 105.482 5.34533 106.494 5.34533Z" fill="currentColor"/>
|
||||
<path d="M24.2577 16.4436V21.9248H10.6705C6.71194 21.9248 3.25559 19.7204 1.40636 16.4436C1.13754 15.9672 0.90225 15.4674 0.704883 14.9487C0.31744 13.9322 0.0738912 12.8415 0 11.7034V10.2214C0.0160422 9.96781 0.0413207 9.71617 0.0743773 9.46752C0.141949 8.95727 0.244035 8.45799 0.378206 7.97265C1.64748 3.37143 5.77469 0 10.6705 0C15.5662 0 19.693 3.37143 20.9622 7.97265H15.1526C14.1988 6.47279 12.5479 5.4812 10.6705 5.4812C8.79305 5.4812 7.14216 6.47279 6.18839 7.97265C5.89768 8.42859 5.67212 8.93136 5.52434 9.46752C5.39308 9.94289 5.32308 10.4442 5.32308 10.9624C5.32308 12.5335 5.96768 13.9497 7.00119 14.9487C7.95886 15.876 9.25 16.4436 10.6705 16.4436H24.2577Z" fill="#FD366E"/>
|
||||
<path d="M24.2578 9.46753V14.9487H14.3398C15.3733 13.9497 16.018 12.5335 16.018 10.9624C16.018 10.4442 15.9479 9.9429 15.8167 9.46753H24.2578Z" fill="#FD366E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.1 KiB |
@@ -1,3 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M5 8C5 6.34315 6.34315 5 8 5C9.65685 5 11 6.34315 11 8C11 9.65685 9.65685 11 8 11C6.34315 11 5 9.65685 5 8Z" fill="#D8D8DB"/>
|
||||
<path d="M5 8C5 6.34315 6.34315 5 8 5C9.65685 5 11 6.34315 11 8C11 9.65685 9.65685 11 8 11C6.34315 11 5 9.65685 5 8Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 261 B After Width: | Height: | Size: 266 B |
@@ -12,7 +12,7 @@
|
||||
const { validateInstallRequest } = window.InstallerStepsProgress || {};
|
||||
|
||||
const isUpgrade = document.body?.dataset.upgrade === 'true';
|
||||
const stepFlow = isUpgrade ? [1, 4, 5] : [1, 2, 3, 4, 5];
|
||||
const stepFlow = isUpgrade ? [1, 6, 4, 5] : [1, 2, 3, 4, 5];
|
||||
const cardSteps = stepFlow.filter((step) => step !== 5);
|
||||
|
||||
const normalizeStep = (step) => {
|
||||
@@ -53,7 +53,7 @@
|
||||
let pendingStep = null;
|
||||
let pendingPushState = false;
|
||||
|
||||
const clampStep = (step) => Math.max(1, Math.min(5, step));
|
||||
const clampStep = (step) => Math.max(1, Math.min(6, step));
|
||||
const isInstallLocked = () => Boolean(window.InstallerSteps?.isInstallLocked?.());
|
||||
|
||||
const scrollToFirstError = (panel) => {
|
||||
@@ -399,11 +399,18 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
if (action === 'next' && String(target) === '5' && typeof validateInstallRequest === 'function') {
|
||||
const isValid = await validateInstallRequest();
|
||||
if (!isValid) {
|
||||
return;
|
||||
if (action === 'next' && String(target) === '5') {
|
||||
if (typeof validateInstallRequest === 'function') {
|
||||
const isValid = await validateInstallRequest();
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Clear stale install data from previous runs so initStep5
|
||||
// starts a fresh install instead of trying to resume.
|
||||
const { clearInstallLock, clearInstallId } = window.InstallerStepsState || {};
|
||||
clearInstallLock?.();
|
||||
clearInstallId?.();
|
||||
}
|
||||
if (isInstallLocked() && Number(target) !== 5) {
|
||||
requestStep(5, true);
|
||||
|
||||
@@ -2,13 +2,21 @@
|
||||
const getBodyDataset = () => document.body?.dataset ?? {};
|
||||
const isUpgradeMode = () => getBodyDataset().upgrade === 'true';
|
||||
const getLockedDatabase = () => getBodyDataset().lockedDatabase || '';
|
||||
const getEnabledDatabases = () => {
|
||||
const raw = getBodyDataset().enabledDatabases;
|
||||
if (!raw) return ['mongodb', 'mariadb', 'postgresql'];
|
||||
try { return JSON.parse(raw); } catch (e) { return ['mongodb', 'mariadb', 'postgresql']; }
|
||||
};
|
||||
|
||||
const STEP_IDS = Object.freeze({
|
||||
CONFIG_FILES: 'config-files',
|
||||
DOCKER_COMPOSE: 'docker-compose',
|
||||
ENV_VARS: 'env-vars',
|
||||
DOCKER_CONTAINERS: 'docker-containers',
|
||||
ACCOUNT_SETUP: 'account-setup'
|
||||
ACCOUNT_SETUP: 'account-setup',
|
||||
MIGRATION: 'migration',
|
||||
SSL_CERTIFICATE: 'ssl-certificate',
|
||||
REDIRECT: 'redirect'
|
||||
});
|
||||
|
||||
const STATUS = Object.freeze({
|
||||
@@ -45,6 +53,11 @@
|
||||
id: STEP_IDS.DOCKER_CONTAINERS,
|
||||
inProgress: 'Restarting Docker containers...',
|
||||
done: 'Docker containers restarted'
|
||||
},
|
||||
{
|
||||
id: STEP_IDS.MIGRATION,
|
||||
inProgress: 'Running database migration...',
|
||||
done: 'Database migration completed'
|
||||
}
|
||||
] : [
|
||||
{
|
||||
@@ -70,7 +83,7 @@
|
||||
{
|
||||
id: STEP_IDS.ACCOUNT_SETUP,
|
||||
inProgress: 'Creating Appwrite account...',
|
||||
done: 'Appwrite account created (redirecting...)'
|
||||
done: 'Appwrite account created'
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -88,13 +101,14 @@
|
||||
const clampStep = (step) => {
|
||||
const numeric = Number(step);
|
||||
if (Number.isNaN(numeric)) return 1;
|
||||
return Math.max(1, Math.min(5, numeric));
|
||||
return Math.max(1, Math.min(6, numeric));
|
||||
};
|
||||
|
||||
window.InstallerStepsContext = Object.freeze({
|
||||
getBodyDataset,
|
||||
isUpgradeMode,
|
||||
getLockedDatabase,
|
||||
getEnabledDatabases,
|
||||
STEP_IDS,
|
||||
STATUS,
|
||||
SSE_EVENTS,
|
||||
|
||||
@@ -21,12 +21,13 @@
|
||||
storeInstallId,
|
||||
clearInstallId
|
||||
} = window.InstallerStepsState || {};
|
||||
const { extractHostname, isLocalHost } = window.InstallerStepsValidation || {};
|
||||
const { extractHostname, isLocalHost, isIPAddress } = window.InstallerStepsValidation || {};
|
||||
const { generateSecretKey } = window.InstallerStepsUI || {};
|
||||
const { showToast } = window.InstallerToast || {};
|
||||
|
||||
let activeInstall = null;
|
||||
let unloadGuard = null;
|
||||
let sseSessionDetails = null;
|
||||
const csrfToken = document.querySelector('meta[name="appwrite-installer-csrf"]')?.getAttribute('content') || '';
|
||||
|
||||
const withCsrfHeader = (headers = {}) => {
|
||||
@@ -110,10 +111,10 @@
|
||||
return normalized.summary || 'Installation failed.';
|
||||
}
|
||||
if (status === STATUS.COMPLETED) return step.done;
|
||||
return step.inProgress;
|
||||
return message || step.inProgress;
|
||||
};
|
||||
|
||||
const updateInstallRow = (row, step, status, message) => {
|
||||
const updateInstallRow = (row, step, status, message, details) => {
|
||||
if (!row || !step) return;
|
||||
row.dataset.status = status;
|
||||
row.dataset.step = step.id;
|
||||
@@ -137,6 +138,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
const counter = row.querySelector('[data-install-counter]');
|
||||
if (counter) {
|
||||
const started = details?.containerStarted ?? 0;
|
||||
const total = details?.containerTotal;
|
||||
counter.textContent = (status === STATUS.IN_PROGRESS && total > 0 && started < total)
|
||||
? `${started}/${total}`
|
||||
: '';
|
||||
}
|
||||
|
||||
// Show/hide "Navigate to Console" button for account setup errors
|
||||
const consoleBtn = row.querySelector('[data-install-console]');
|
||||
if (consoleBtn) {
|
||||
@@ -250,7 +260,7 @@
|
||||
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
||||
};
|
||||
|
||||
const buildRedirectUrl = () => {
|
||||
const buildRedirectUrl = (protocol) => {
|
||||
const dataset = getBodyDataset?.() ?? {};
|
||||
const rawDomain = (formState?.appDomain || dataset.defaultAppDomain || '').trim();
|
||||
if (!rawDomain) return '';
|
||||
@@ -265,22 +275,53 @@
|
||||
} else if (normalizedHost === 'traefik') {
|
||||
host = rawDomain.replace(hostForProtocol, 'localhost');
|
||||
}
|
||||
let protocol = 'http';
|
||||
let port = httpPort;
|
||||
if (httpsPort && httpsPort !== '0' && !isLocalHost?.(normalizedHost)) {
|
||||
protocol = 'https';
|
||||
port = httpsPort;
|
||||
}
|
||||
if (!hasPort && port && ((protocol === 'http' && port !== '80') || (protocol === 'https' && port !== '443'))) {
|
||||
const port = protocol === 'https' ? httpsPort : httpPort;
|
||||
const defaultPort = protocol === 'https' ? '443' : '80';
|
||||
if (!hasPort && port && port !== defaultPort) {
|
||||
host = `${host}:${port}`;
|
||||
}
|
||||
return `${protocol}://${host}`;
|
||||
};
|
||||
|
||||
const redirectToApp = () => {
|
||||
const url = buildRedirectUrl();
|
||||
const normalizeHostname = (rawDomain) => {
|
||||
const hostname = extractHostname?.(rawDomain)?.toLowerCase?.() ?? '';
|
||||
if (hostname === '0.0.0.0' || hostname === 'traefik') return 'localhost';
|
||||
return hostname;
|
||||
};
|
||||
|
||||
const canUseHttps = () => {
|
||||
const dataset = getBodyDataset?.() ?? {};
|
||||
const rawDomain = (formState?.appDomain || dataset.defaultAppDomain || '').trim();
|
||||
const httpsPort = (formState?.httpsPort || dataset.defaultHttpsPort || '').trim();
|
||||
if (!httpsPort || httpsPort === '0') return false;
|
||||
const hostname = normalizeHostname(rawDomain);
|
||||
return !isLocalHost?.(hostname) && !isIPAddress?.(hostname);
|
||||
};
|
||||
|
||||
const pollCertificate = async (domain, port, maxAttempts, intervalMs) => {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/install/certificate?domain=${encodeURIComponent(domain)}&port=${encodeURIComponent(port)}`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.ready) return true;
|
||||
}
|
||||
} catch {
|
||||
// Installer server may have shut down
|
||||
}
|
||||
if (i < maxAttempts - 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const redirectToApp = (protocol) => {
|
||||
const url = buildRedirectUrl(protocol);
|
||||
if (!url) return;
|
||||
// Fire-and-forget: tell the installer server it can shut down
|
||||
fetch('/install/shutdown', { method: 'POST', headers: withCsrfHeader() }).catch(() => {});
|
||||
window.location.href = url;
|
||||
};
|
||||
@@ -317,7 +358,7 @@
|
||||
const normalizedDomain = (formState?.appDomain || '').trim() || 'localhost';
|
||||
const normalizedHttpPort = (formState?.httpPort || '').trim() || '80';
|
||||
const normalizedHttpsPort = (formState?.httpsPort || '').trim() || '443';
|
||||
const normalizedEmail = (formState?.emailCertificates || '').trim();
|
||||
const normalizedEmail = (formState?.emailCertificates || '').trim() || (formState?.accountEmail || '').trim();
|
||||
const normalizedAssistantKey = (formState?.assistantOpenAIKey || '').trim();
|
||||
const normalizedAccountEmail = (formState?.accountEmail || '').trim();
|
||||
const normalizedAccountPassword = (formState?.accountPassword || '').trim();
|
||||
@@ -332,7 +373,8 @@
|
||||
opensslKey: (formState?.opensslKey || '').trim(),
|
||||
assistantOpenAIKey: normalizedAssistantKey,
|
||||
accountEmail: normalizedAccountEmail,
|
||||
accountPassword: normalizedAccountPassword
|
||||
accountPassword: normalizedAccountPassword,
|
||||
migrate: formState?.migrate ?? false
|
||||
};
|
||||
};
|
||||
|
||||
@@ -405,6 +447,7 @@
|
||||
|
||||
const initStep5 = (root) => {
|
||||
if (!root) return;
|
||||
let resolvedProtocol = 'http';
|
||||
|
||||
if (activeInstall?.controller) {
|
||||
activeInstall.controller.abort();
|
||||
@@ -496,7 +539,7 @@
|
||||
if (!state) return;
|
||||
const row = ensureRow(step);
|
||||
if (row) {
|
||||
updateInstallRow(row, step, state.status || STATUS.IN_PROGRESS, state.message);
|
||||
updateInstallRow(row, step, state.status || STATUS.IN_PROGRESS, state.message, state.details);
|
||||
if (state.status === STATUS?.ERROR) {
|
||||
updateInstallErrorDetails(row, {
|
||||
message: state.message,
|
||||
@@ -524,6 +567,9 @@
|
||||
inProgress: payload.message || payload.step,
|
||||
done: payload.message || payload.step
|
||||
};
|
||||
if (step.id === STEP_IDS.ACCOUNT_SETUP && payload.details?.sessionSecret) {
|
||||
sseSessionDetails = payload.details;
|
||||
}
|
||||
progressState.set(step.id, {
|
||||
status: payload.status || STATUS.IN_PROGRESS,
|
||||
message: payload.message,
|
||||
@@ -543,6 +589,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
if (payload.status === STATUS.ERROR) {
|
||||
showGlobalActions();
|
||||
}
|
||||
scheduleFallback();
|
||||
};
|
||||
|
||||
@@ -580,6 +629,7 @@
|
||||
|
||||
const applySnapshot = (snapshot) => {
|
||||
if (!snapshot || !snapshot.steps) return;
|
||||
let hasErrors = false;
|
||||
INSTALLATION_STEPS.forEach((step) => {
|
||||
const detail = snapshot.steps[step.id];
|
||||
if (!detail) return;
|
||||
@@ -588,8 +638,14 @@
|
||||
message: detail.message,
|
||||
details: snapshot.details?.[step.id]
|
||||
});
|
||||
if (detail.status === STATUS.ERROR) {
|
||||
hasErrors = true;
|
||||
}
|
||||
});
|
||||
renderProgress();
|
||||
if (hasErrors) {
|
||||
showGlobalActions();
|
||||
}
|
||||
};
|
||||
|
||||
const checkAllCompleted = () => {
|
||||
@@ -599,11 +655,9 @@
|
||||
});
|
||||
if (!allDone) return;
|
||||
const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP);
|
||||
const sessionDetails = accountState?.details;
|
||||
const sessionDetails = sseSessionDetails || accountState?.details;
|
||||
finalizeInstall();
|
||||
notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
|
||||
setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0);
|
||||
});
|
||||
startSslCheck(sessionDetails);
|
||||
};
|
||||
|
||||
const startPolling = () => {
|
||||
@@ -640,6 +694,78 @@
|
||||
}
|
||||
stopSyncedSpinnerRotation();
|
||||
setUnloadGuard(false);
|
||||
clearInstallLock?.();
|
||||
};
|
||||
|
||||
const SSL_STEP = {
|
||||
id: STEP_IDS.SSL_CERTIFICATE,
|
||||
inProgress: 'Generating SSL certificate...',
|
||||
done: 'SSL certificate verified'
|
||||
};
|
||||
|
||||
const REDIRECT_STEP = {
|
||||
id: STEP_IDS.REDIRECT,
|
||||
inProgress: 'Redirecting to console...',
|
||||
done: 'Redirecting to console...'
|
||||
};
|
||||
|
||||
const showRedirectStep = (sessionDetails, protocol) => {
|
||||
animatePanelHeight(() => {
|
||||
progressState.set(REDIRECT_STEP.id, {
|
||||
status: STATUS.IN_PROGRESS,
|
||||
message: REDIRECT_STEP.inProgress
|
||||
});
|
||||
const row = ensureRow(REDIRECT_STEP);
|
||||
if (row) {
|
||||
updateInstallRow(row, REDIRECT_STEP, STATUS.IN_PROGRESS, REDIRECT_STEP.inProgress);
|
||||
}
|
||||
});
|
||||
startSyncedSpinnerRotation(list);
|
||||
|
||||
const completeId = activeInstall?.installId || getStoredInstallId?.();
|
||||
notifyInstallComplete(completeId, sessionDetails).finally(() => {
|
||||
setTimeout(() => redirectToApp(protocol), TIMINGS?.redirectDelay ?? 0);
|
||||
});
|
||||
};
|
||||
|
||||
const startSslCheck = (sessionDetails) => {
|
||||
if (!canUseHttps()) {
|
||||
showRedirectStep(sessionDetails, 'http');
|
||||
return;
|
||||
}
|
||||
|
||||
animatePanelHeight(() => {
|
||||
progressState.set(SSL_STEP.id, {
|
||||
status: STATUS.IN_PROGRESS,
|
||||
message: SSL_STEP.inProgress
|
||||
});
|
||||
const row = ensureRow(SSL_STEP);
|
||||
if (row) {
|
||||
updateInstallRow(row, SSL_STEP, STATUS.IN_PROGRESS, SSL_STEP.inProgress);
|
||||
}
|
||||
});
|
||||
startSyncedSpinnerRotation(list);
|
||||
|
||||
const dataset = getBodyDataset?.() ?? {};
|
||||
const rawDomain = (formState?.appDomain || dataset.defaultAppDomain || '').trim();
|
||||
const httpsPort = (formState?.httpsPort || dataset.defaultHttpsPort || '443').trim();
|
||||
const domain = normalizeHostname(rawDomain);
|
||||
pollCertificate(domain, httpsPort, 15, 2000).then((ready) => {
|
||||
stopSyncedSpinnerRotation();
|
||||
const certMessage = ready ? SSL_STEP.done : 'Certificate not ready, continuing over HTTP';
|
||||
animatePanelHeight(() => {
|
||||
progressState.set(SSL_STEP.id, {
|
||||
status: STATUS.COMPLETED,
|
||||
message: certMessage
|
||||
});
|
||||
const row = ensureRow(SSL_STEP);
|
||||
if (row) {
|
||||
updateInstallRow(row, SSL_STEP, STATUS.COMPLETED, certMessage);
|
||||
}
|
||||
});
|
||||
resolvedProtocol = ready ? 'https' : 'http';
|
||||
showRedirectStep(sessionDetails, resolvedProtocol);
|
||||
});
|
||||
};
|
||||
|
||||
const startInstallStream = async (installId, options = {}) => {
|
||||
@@ -740,11 +866,9 @@
|
||||
}
|
||||
|
||||
const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP);
|
||||
const sessionDetails = accountState?.details;
|
||||
const sessionDetails = sseSessionDetails || accountState?.details;
|
||||
finalizeInstall();
|
||||
notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
|
||||
setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0);
|
||||
});
|
||||
startSslCheck(sessionDetails);
|
||||
return;
|
||||
}
|
||||
if (event === SSE_EVENTS.ERROR) {
|
||||
@@ -788,9 +912,29 @@
|
||||
}
|
||||
};
|
||||
|
||||
const isSnapshotTerminal = (snapshot) => {
|
||||
if (!snapshot?.steps) return 'empty';
|
||||
const stepEntries = Object.values(snapshot.steps);
|
||||
if (stepEntries.length === 0) return 'empty';
|
||||
const hasError = stepEntries.some((s) => s.status === STATUS.ERROR);
|
||||
if (hasError) return 'error';
|
||||
const allCompleted = INSTALLATION_STEPS.every((step) => {
|
||||
const detail = snapshot.steps[step.id];
|
||||
return detail && detail.status === STATUS.COMPLETED;
|
||||
});
|
||||
if (allCompleted) return 'completed';
|
||||
return false;
|
||||
};
|
||||
|
||||
const resumeInstall = async (installId) => {
|
||||
const snapshot = await fetchInstallStatus(installId);
|
||||
if (!snapshot) return false;
|
||||
const terminal = isSnapshotTerminal(snapshot);
|
||||
if (!snapshot || terminal) {
|
||||
if (terminal === 'completed') {
|
||||
return 'completed';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
activeInstall = {
|
||||
installId,
|
||||
controller: new AbortController(),
|
||||
@@ -853,7 +997,7 @@
|
||||
const retryButton = event.target.closest('[data-install-retry]');
|
||||
|
||||
if (consoleButton) {
|
||||
redirectToApp();
|
||||
redirectToApp(resolvedProtocol);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -864,6 +1008,60 @@
|
||||
}
|
||||
});
|
||||
|
||||
const globalActions = root.querySelector('[data-install-global-actions]');
|
||||
|
||||
const showGlobalActions = () => {
|
||||
if (globalActions) {
|
||||
globalActions.classList.remove('is-hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const performReset = async (hard) => {
|
||||
const installId = activeInstall?.installId || getInstallLock?.()?.installId || getStoredInstallId?.();
|
||||
|
||||
try {
|
||||
const res = await fetch('/install/reset', {
|
||||
method: 'POST',
|
||||
headers: withCsrfHeader({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify({ installId: installId || '', hard })
|
||||
});
|
||||
if (hard && !res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
showToast?.({
|
||||
status: 'error',
|
||||
title: 'Reset failed',
|
||||
description: data?.message || 'Could not stop containers. Try running "docker compose down -v" manually.',
|
||||
dismissible: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Reset request failed:', e);
|
||||
}
|
||||
|
||||
clearInstallLock?.();
|
||||
clearInstallId?.();
|
||||
cleanupInstallFlow();
|
||||
window.location.href = '/?step=1';
|
||||
};
|
||||
|
||||
const startOverButton = root.querySelector('[data-install-start-over]');
|
||||
if (startOverButton) {
|
||||
startOverButton.addEventListener('click', () => performReset(false));
|
||||
}
|
||||
|
||||
const hardResetButton = root.querySelector('[data-install-hard-reset]');
|
||||
if (hardResetButton) {
|
||||
hardResetButton.addEventListener('click', () => {
|
||||
const confirmed = window.confirm(
|
||||
'This will stop all containers, remove all volumes (including database data, uploads, and certificates), and delete configuration files.\n\nThis action cannot be undone. Continue?'
|
||||
);
|
||||
if (confirmed) {
|
||||
performReset(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// When the user switches back to this tab, check if installation
|
||||
// finished while the tab was in the background.
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
@@ -872,22 +1070,45 @@
|
||||
}
|
||||
});
|
||||
|
||||
const lock = getInstallLock?.();
|
||||
const existingInstallId = lock?.installId || getStoredInstallId?.();
|
||||
if (existingInstallId) {
|
||||
resumeInstall(existingInstallId).then((resumed) => {
|
||||
if (!resumed) {
|
||||
clearInstallId?.();
|
||||
clearInstallLock?.();
|
||||
const newInstallId = generateInstallId();
|
||||
storeInstallId?.(newInstallId);
|
||||
startInstallStream(newInstallId);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const startFreshInstall = () => {
|
||||
clearInstallId?.();
|
||||
clearInstallLock?.();
|
||||
const newInstallId = generateInstallId();
|
||||
storeInstallId?.(newInstallId);
|
||||
startInstallStream(newInstallId);
|
||||
};
|
||||
|
||||
const recoverToLastStep = () => {
|
||||
clearInstallId?.();
|
||||
clearInstallLock?.();
|
||||
const url = new URL(window.location.href);
|
||||
const lastStep = url.searchParams.get('step');
|
||||
// Stay on the current URL so the user keeps their place;
|
||||
// only navigate away if we're already on step 5 (the
|
||||
// progress screen) since there's nothing to show.
|
||||
if (!lastStep || String(lastStep) === '5') {
|
||||
window.location.href = '/?step=1';
|
||||
}
|
||||
};
|
||||
|
||||
const lock = getInstallLock?.();
|
||||
const existingInstallId = lock?.installId || getStoredInstallId?.();
|
||||
if (existingInstallId) {
|
||||
resumeInstall(existingInstallId).then((result) => {
|
||||
if (result === 'completed') {
|
||||
// Install already finished — redirect to console
|
||||
// instead of bouncing back to step 1.
|
||||
stopSyncedSpinnerRotation();
|
||||
setUnloadGuard(false);
|
||||
clearInstallLock?.();
|
||||
clearInstallId?.();
|
||||
startSslCheck(null);
|
||||
} else if (!result) {
|
||||
recoverToLastStep();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
startFreshInstall();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
const INSTALL_LOCK_KEY = 'appwrite-install-lock';
|
||||
const INSTALL_ID_KEY = 'appwrite-install-id';
|
||||
const INSTALL_LOCK_LOCAL_KEY = 'appwrite-install-lock-backup';
|
||||
const INSTALL_ID_LOCAL_KEY = 'appwrite-install-id-backup';
|
||||
|
||||
const formState = {
|
||||
appDomain: null,
|
||||
@@ -55,13 +57,24 @@
|
||||
const getInstallLock = () => {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(INSTALL_LOCK_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || typeof parsed !== 'object') return null;
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === 'object') return parsed;
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(INSTALL_LOCK_LOCAL_KEY);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
sessionStorage.setItem(INSTALL_LOCK_KEY, raw);
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const setInstallLock = (installId, payload) => {
|
||||
@@ -79,6 +92,9 @@
|
||||
try {
|
||||
sessionStorage.setItem(INSTALL_LOCK_KEY, JSON.stringify(lock));
|
||||
} catch (error) {}
|
||||
try {
|
||||
localStorage.setItem(INSTALL_LOCK_LOCAL_KEY, JSON.stringify(lock));
|
||||
} catch (error) {}
|
||||
if (document.body) {
|
||||
document.body.dataset.installLocked = 'true';
|
||||
}
|
||||
@@ -89,6 +105,9 @@
|
||||
try {
|
||||
sessionStorage.removeItem(INSTALL_LOCK_KEY);
|
||||
} catch (error) {}
|
||||
try {
|
||||
localStorage.removeItem(INSTALL_LOCK_LOCAL_KEY);
|
||||
} catch (error) {}
|
||||
if (document.body) {
|
||||
delete document.body.dataset.installLocked;
|
||||
}
|
||||
@@ -121,22 +140,31 @@
|
||||
|
||||
const getStoredInstallId = () => {
|
||||
try {
|
||||
return sessionStorage.getItem(INSTALL_ID_KEY);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
const val = sessionStorage.getItem(INSTALL_ID_KEY);
|
||||
if (val) return val;
|
||||
} catch (error) {}
|
||||
try {
|
||||
return localStorage.getItem(INSTALL_ID_LOCAL_KEY);
|
||||
} catch (error) {}
|
||||
return null;
|
||||
};
|
||||
|
||||
const storeInstallId = (installId) => {
|
||||
try {
|
||||
sessionStorage.setItem(INSTALL_ID_KEY, installId);
|
||||
} catch (error) {}
|
||||
try {
|
||||
localStorage.setItem(INSTALL_ID_LOCAL_KEY, installId);
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
const clearInstallId = () => {
|
||||
try {
|
||||
sessionStorage.removeItem(INSTALL_ID_KEY);
|
||||
} catch (error) {}
|
||||
try {
|
||||
localStorage.removeItem(INSTALL_ID_LOCAL_KEY);
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
window.InstallerStepsState = {
|
||||
|
||||
@@ -240,6 +240,9 @@
|
||||
if (key === 'database') {
|
||||
value = toDatabaseLabel(formState?.database);
|
||||
}
|
||||
if (key === 'emailCertificates' && !value) {
|
||||
value = formState?.accountEmail;
|
||||
}
|
||||
if (value) {
|
||||
node.textContent = value;
|
||||
}
|
||||
|
||||
@@ -106,12 +106,18 @@
|
||||
return LOCAL_HOSTS.has(normalized);
|
||||
};
|
||||
|
||||
const isIPAddress = (host) => {
|
||||
if (!host) return false;
|
||||
return isValidIPv4(host) || isValidIPv6(host);
|
||||
};
|
||||
|
||||
window.InstallerStepsValidation = {
|
||||
isValidEmail,
|
||||
isValidPort,
|
||||
isValidPassword,
|
||||
isValidHostnameInput,
|
||||
extractHostname,
|
||||
isLocalHost
|
||||
isLocalHost,
|
||||
isIPAddress
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
const {
|
||||
INSTALLATION_STEPS,
|
||||
clampStep,
|
||||
isUpgradeMode
|
||||
isUpgradeMode,
|
||||
getEnabledDatabases
|
||||
} = Context;
|
||||
|
||||
const {
|
||||
@@ -79,6 +80,19 @@
|
||||
}
|
||||
};
|
||||
|
||||
const applyEnabledDatabases = (root) => {
|
||||
const enabled = getEnabledDatabases?.() || [];
|
||||
const radios = root.querySelectorAll('input[name="database"]');
|
||||
radios.forEach((radio) => {
|
||||
if (!enabled.includes(radio.value)) {
|
||||
const card = radio.closest('.selector-card');
|
||||
if (card) {
|
||||
card.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const bindDatabaseSelection = (root) => {
|
||||
const radios = root.querySelectorAll('input[name="database"]');
|
||||
radios.forEach((radio) => {
|
||||
@@ -139,6 +153,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
applyEnabledDatabases(root);
|
||||
|
||||
const lockedDatabase = getLockedDatabase?.() || '';
|
||||
if (lockedDatabase) {
|
||||
lockDatabaseSelection(root, lockedDatabase);
|
||||
@@ -313,6 +329,30 @@
|
||||
}
|
||||
};
|
||||
|
||||
const initStep6 = (root) => {
|
||||
if (!root) return;
|
||||
syncInstallLockFlag?.();
|
||||
applyLockPayload?.();
|
||||
applyBodyDefaults?.();
|
||||
|
||||
const checkbox = root.querySelector('#run-migration');
|
||||
if (checkbox) {
|
||||
if (formState.migrate !== undefined) {
|
||||
checkbox.checked = formState.migrate;
|
||||
} else {
|
||||
formState.migrate = checkbox.checked;
|
||||
}
|
||||
checkbox.addEventListener('change', () => {
|
||||
formState.migrate = checkbox.checked;
|
||||
dispatchStateChange?.('migrate');
|
||||
});
|
||||
}
|
||||
|
||||
if (isInstallLocked?.()) {
|
||||
disableControls?.(root);
|
||||
}
|
||||
};
|
||||
|
||||
const initStep = (step, container) => {
|
||||
if (!container) return;
|
||||
const root = container.querySelector('.step-layout') || container;
|
||||
@@ -330,6 +370,7 @@
|
||||
if (normalized === 3) initStep3(root);
|
||||
if (normalized === 4) initStep4(root);
|
||||
if (normalized === 5) Progress.initStep5?.(root);
|
||||
if (normalized === 6) initStep6(root);
|
||||
};
|
||||
|
||||
window.InstallerSteps = {
|
||||
@@ -374,10 +415,7 @@
|
||||
if (!parsePort(httpPort, 'HTTP')) valid = false;
|
||||
if (!parsePort(httpsPort, 'HTTPS')) valid = false;
|
||||
|
||||
if (!sslEmail || !sslEmail.value.trim()) {
|
||||
setFieldError?.(sslEmail, 'Please enter an email address for SSL certificates');
|
||||
valid = false;
|
||||
} else if (!isValidEmail?.(sslEmail.value.trim())) {
|
||||
if (sslEmail && sslEmail.value.trim() && !isValidEmail?.(sslEmail.value.trim())) {
|
||||
setFieldError?.(sslEmail, 'Please enter a valid email address');
|
||||
valid = false;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,12 @@ $defaultHttpsPort = $defaultHttpsPort ?? '443';
|
||||
$defaultEmailCertificates = $defaultEmailCertificates ?? '';
|
||||
$defaultAssistantOpenAIKey = $defaultAssistantOpenAIKey ?? '';
|
||||
$defaultDatabase = $defaultDatabase ?? 'mongodb';
|
||||
$enabledDatabases = $enabledDatabases ?? ['mongodb', 'mariadb', 'postgresql'];
|
||||
$selectedDatabase = $lockedDatabase ?: $defaultDatabase;
|
||||
$isDatabaseLocked = !empty($lockedDatabase);
|
||||
$mongoEnabled = in_array('mongodb', $enabledDatabases, true);
|
||||
$mariaEnabled = in_array('mariadb', $enabledDatabases, true);
|
||||
$postgresEnabled = in_array('postgresql', $enabledDatabases, true);
|
||||
$mongoDisabled = $isDatabaseLocked && $selectedDatabase !== 'mongodb';
|
||||
$mariaDisabled = $isDatabaseLocked && $selectedDatabase !== 'mariadb';
|
||||
$postgresDisabled = $isDatabaseLocked && $selectedDatabase !== 'postgresql';
|
||||
@@ -43,6 +47,7 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
|
||||
<div class="input-group stack-xs">
|
||||
<label class="label-text typography-text-m-500 text-neutral-secondary">Database</label>
|
||||
<div class="selector-group<?php echo $isDatabaseLocked ? ' is-locked' : ''; ?>">
|
||||
<?php if ($mongoEnabled) { ?>
|
||||
<label class="selector-card <?php echo ($selectedDatabase === 'mongodb') ? 'selected' : ''; ?><?php echo $mongoDisabled ? ' is-disabled has-tooltip' : ''; ?>"<?php echo $mongoDisabled ? ' aria-disabled="true"' : ''; ?>>
|
||||
<input type="radio" name="database" value="mongodb" <?php echo ($selectedDatabase === 'mongodb') ? 'checked' : ''; ?> class="sr-only" <?php echo $mongoDisabled ? 'disabled' : ''; ?>>
|
||||
<div class="selector-content">
|
||||
@@ -54,7 +59,9 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
|
||||
<span class="tooltip tooltip-db-locked typography-text-m-400 text-on-invert" role="tooltip" data-tooltip-portal="true">Database cannot be changed after initial setup.</span>
|
||||
<?php } ?>
|
||||
</label>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($mariaEnabled) { ?>
|
||||
<label class="selector-card <?php echo ($selectedDatabase === 'mariadb') ? 'selected' : ''; ?><?php echo $mariaDisabled ? ' is-disabled has-tooltip' : ''; ?>"<?php echo $mariaDisabled ? ' aria-disabled="true"' : ''; ?>>
|
||||
<input type="radio" name="database" value="mariadb" <?php echo ($selectedDatabase === 'mariadb') ? 'checked' : ''; ?> class="sr-only" <?php echo $mariaDisabled ? 'disabled' : ''; ?>>
|
||||
<div class="selector-content">
|
||||
@@ -66,7 +73,9 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
|
||||
<span class="tooltip tooltip-db-locked typography-text-m-400 text-on-invert" role="tooltip" data-tooltip-portal="true">Database cannot be changed after initial setup.</span>
|
||||
<?php } ?>
|
||||
</label>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($postgresEnabled) { ?>
|
||||
<label class="selector-card <?php echo ($selectedDatabase === 'postgresql') ? 'selected' : ''; ?><?php echo $postgresDisabled ? ' is-disabled has-tooltip' : ''; ?>"<?php echo $postgresDisabled ? ' aria-disabled="true"' : ''; ?>>
|
||||
<input type="radio" name="database" value="postgresql" <?php echo ($selectedDatabase === 'postgresql') ? 'checked' : ''; ?> class="sr-only" <?php echo $postgresDisabled ? 'disabled' : ''; ?>>
|
||||
<div class="selector-content">
|
||||
@@ -78,6 +87,7 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
|
||||
<span class="tooltip tooltip-db-locked typography-text-m-400 text-on-invert" role="tooltip" data-tooltip-portal="true">Database cannot be changed after initial setup.</span>
|
||||
<?php } ?>
|
||||
</label>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -62,12 +62,14 @@ $badgeClass = $defaultSecretKey !== '' ? 'badge-success' : 'badge-warning';
|
||||
<span class="badge badge-neutral typography-text-xs-400" data-review-assistant-badge>Disabled</span>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Appwrite Assistant</div>
|
||||
</div>
|
||||
<?php if (!$isUpgrade) { ?>
|
||||
<div class="review-row">
|
||||
<span class="badge <?php echo $badgeClass; ?> typography-text-xs-400" data-review-badge>
|
||||
<?php echo htmlspecialchars((string) $badgeLabel, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</span>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Secret API key</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ $isUpgrade = $isUpgrade ?? false;
|
||||
<div class="install-panel">
|
||||
<div class="install-header">
|
||||
<div class="typography-text-m-400 text-neutral-primary">
|
||||
<?php echo $isUpgrade ? 'Updating your app…' : 'Installing your app…'; ?>
|
||||
<?php echo $isUpgrade ? 'Updating Appwrite…' : 'Installing Appwrite…'; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="install-list" data-install-list></div>
|
||||
@@ -30,6 +30,7 @@ $isUpgrade = $isUpgrade ?? false;
|
||||
</span>
|
||||
<span class="install-text typography-text-m-400 text-neutral-primary" data-install-text></span>
|
||||
</div>
|
||||
<span class="install-counter typography-text-xs-400" data-install-counter></span>
|
||||
<button type="button" class="install-row-toggle" aria-expanded="false" data-install-toggle>
|
||||
<?php include __DIR__ . '/../../icons/chevron-down.svg'; ?>
|
||||
</button>
|
||||
@@ -50,4 +51,13 @@ $isUpgrade = $isUpgrade ?? false;
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="install-global-actions is-hidden" data-install-global-actions>
|
||||
<button type="button" class="button secondary" data-install-start-over>
|
||||
<span class="button-text typography-text-m-500">Start Over</span>
|
||||
</button>
|
||||
<button type="button" class="button secondary" data-install-hard-reset>
|
||||
<span class="button-text typography-text-m-500">Reset Everything</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
$isUpgrade = $isUpgrade ?? false;
|
||||
?>
|
||||
<div class="step-layout" data-step="6">
|
||||
<div class="stack-xl">
|
||||
<div class="stack-xxxs">
|
||||
<h1 class="typography-title-s text-neutral-primary">Database migration</h1>
|
||||
<p class="typography-text-m-400 text-neutral-secondary">
|
||||
Run database migration after the update to apply schema changes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="stack-xl">
|
||||
<label class="migration-option" for="run-migration">
|
||||
<span class="migration-option-content">
|
||||
<span class="typography-text-m-500 text-neutral-primary">Run migration automatically</span>
|
||||
<span class="typography-text-xs-400 text-neutral-tertiary">Recommended when upgrading to a new version</span>
|
||||
</span>
|
||||
<span class="migration-switch">
|
||||
<input type="checkbox" id="run-migration" name="migrate" class="sr-only" checked>
|
||||
<span class="migration-switch-track" aria-hidden="true">
|
||||
<span class="migration-switch-thumb"></span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="migration-hint">
|
||||
<span class="migration-hint-icon">
|
||||
<?php include __DIR__ . '/../../icons/info.svg'; ?>
|
||||
</span>
|
||||
<span class="typography-text-xs-400 text-neutral-tertiary">
|
||||
To run manually later: <code class="migration-code">docker compose exec appwrite migrate</code>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+57
-4
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/init.php';
|
||||
|
||||
use Appwrite\Certificates\LetsEncrypt;
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Build;
|
||||
@@ -220,20 +219,74 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authoriza
|
||||
};
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
Server::setResource('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');
|
||||
$pool = $pools->get($databaseDSN->getHost());
|
||||
|
||||
$adapter = new DatabasePool($pool);
|
||||
$database = new Database($adapter, $cache);
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization);
|
||||
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables, true)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant((int) $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 * (int) 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
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * (int) 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
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
|
||||
});
|
||||
|
||||
Server::setResource('cache', function (Registry $register) {
|
||||
|
||||
+7
-14
@@ -13,9 +13,9 @@
|
||||
"test": "vendor/bin/phpunit",
|
||||
"lint": "vendor/bin/pint --test --config pint.json",
|
||||
"format": "vendor/bin/pint --config pint.json",
|
||||
"analyze": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G",
|
||||
"analyze": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G",
|
||||
"bench": "vendor/bin/phpbench run --report=benchmark",
|
||||
"check": "./vendor/bin/phpstan analyse -c phpstan.neon",
|
||||
"check": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G",
|
||||
"installer:clean": "php src/Appwrite/Platform/Installer/Server.php --clean",
|
||||
"installer:dev": "docker compose build && composer installer:clean && php src/Appwrite/Platform/Installer/Server.php --docker"
|
||||
},
|
||||
@@ -52,7 +52,6 @@
|
||||
"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.*",
|
||||
@@ -61,7 +60,8 @@
|
||||
"utopia-php/compression": "0.1.*",
|
||||
"utopia-php/config": "1.*",
|
||||
"utopia-php/console": "0.1.*",
|
||||
"utopia-php/database": "dev-fix-collection-recreate as 5.3.15",
|
||||
"utopia-php/database": "5.*",
|
||||
"utopia-php/agents": "1.*",
|
||||
"utopia-php/detector": "0.2.*",
|
||||
"utopia-php/domains": "1.*",
|
||||
"utopia-php/emails": "0.6.*",
|
||||
@@ -72,8 +72,8 @@
|
||||
"utopia-php/image": "0.8.*",
|
||||
"utopia-php/locale": "0.8.*",
|
||||
"utopia-php/logger": "0.6.*",
|
||||
"utopia-php/messaging": "0.20.*",
|
||||
"utopia-php/migration": "1.7.*",
|
||||
"utopia-php/messaging": "0.22.*",
|
||||
"utopia-php/migration": "1.9.*",
|
||||
"utopia-php/platform": "0.7.*",
|
||||
"utopia-php/pools": "1.*",
|
||||
"utopia-php/span": "1.1.*",
|
||||
@@ -84,7 +84,7 @@
|
||||
"utopia-php/storage": "1.0.*",
|
||||
"utopia-php/system": "0.10.*",
|
||||
"utopia-php/telemetry": "0.2.*",
|
||||
"utopia-php/vcs": "2.*",
|
||||
"utopia-php/vcs": "3.*",
|
||||
"utopia-php/websocket": "1.0.*",
|
||||
"matomo/device-detector": "6.4.*",
|
||||
"dragonmantank/cron-expression": "3.4.*",
|
||||
@@ -97,12 +97,6 @@
|
||||
"enshrined/svg-sanitize": "0.22.*",
|
||||
"utopia-php/di": "0.1.0"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/utopia-php/database"
|
||||
}
|
||||
],
|
||||
"require-dev": {
|
||||
"ext-fileinfo": "*",
|
||||
"appwrite/sdk-generator": "*",
|
||||
@@ -119,7 +113,6 @@
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.3"
|
||||
},
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true,
|
||||
|
||||
Generated
+106
-147
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "13579de3d747c541fdcce4f709df8e57",
|
||||
"content-hash": "4fe91e67f343fbe6deac1fdc7eda949f",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -161,16 +161,16 @@
|
||||
},
|
||||
{
|
||||
"name": "appwrite/php-runtimes",
|
||||
"version": "0.19.4",
|
||||
"version": "0.19.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/runtimes.git",
|
||||
"reference": "eea9d1b3ca2540eab623b419c8afde09ef406c0b"
|
||||
"reference": "aa2f7760cd0493c0880209b92df812c9386b3546"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/runtimes/zipball/eea9d1b3ca2540eab623b419c8afde09ef406c0b",
|
||||
"reference": "eea9d1b3ca2540eab623b419c8afde09ef406c0b",
|
||||
"url": "https://api.github.com/repos/appwrite/runtimes/zipball/aa2f7760cd0493c0880209b92df812c9386b3546",
|
||||
"reference": "aa2f7760cd0493c0880209b92df812c9386b3546",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -210,9 +210,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/runtimes/issues",
|
||||
"source": "https://github.com/appwrite/runtimes/tree/0.19.4"
|
||||
"source": "https://github.com/appwrite/runtimes/tree/0.19.5"
|
||||
},
|
||||
"time": "2026-02-17T10:04:39+00:00"
|
||||
"time": "2026-04-01T01:39:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -686,23 +686,23 @@
|
||||
},
|
||||
{
|
||||
"name": "google/protobuf",
|
||||
"version": "v4.33.5",
|
||||
"version": "v4.33.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/protocolbuffers/protobuf-php.git",
|
||||
"reference": "ebe8010a61b2ae0cff0d246fe1c4d44e9f7dfa6d"
|
||||
"reference": "84b008c23915ed94536737eae46f41ba3bccfe67"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/ebe8010a61b2ae0cff0d246fe1c4d44e9f7dfa6d",
|
||||
"reference": "ebe8010a61b2ae0cff0d246fe1c4d44e9f7dfa6d",
|
||||
"url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/84b008c23915ed94536737eae46f41ba3bccfe67",
|
||||
"reference": "84b008c23915ed94536737eae46f41ba3bccfe67",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": ">=5.0.0 <8.5.27"
|
||||
"phpunit/phpunit": ">=10.5.62 <11.0.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-bcmath": "Need to support JSON deserialization"
|
||||
@@ -724,9 +724,9 @@
|
||||
"proto"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.5"
|
||||
"source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.6"
|
||||
},
|
||||
"time": "2026-01-29T20:49:00+00:00"
|
||||
"time": "2026-03-18T17:32:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "halaxa/json-machine",
|
||||
@@ -1226,16 +1226,16 @@
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/api",
|
||||
"version": "1.8.0",
|
||||
"version": "1.9.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/api.git",
|
||||
"reference": "df5197c6fd0ddd8e9883b87de042d9341300e2ad"
|
||||
"reference": "6f8d237ce2c304ca85f31970f788e7f074d147be"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/api/zipball/df5197c6fd0ddd8e9883b87de042d9341300e2ad",
|
||||
"reference": "df5197c6fd0ddd8e9883b87de042d9341300e2ad",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/api/zipball/6f8d237ce2c304ca85f31970f788e7f074d147be",
|
||||
"reference": "6f8d237ce2c304ca85f31970f788e7f074d147be",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1292,20 +1292,20 @@
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2026-01-21T04:14:03+00:00"
|
||||
"time": "2026-02-25T13:24:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/context",
|
||||
"version": "1.4.0",
|
||||
"version": "1.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/context.git",
|
||||
"reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf"
|
||||
"reference": "3c414b246e0dabb7d6145404e6a5e4536ca18d07"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/context/zipball/d4c4470b541ce72000d18c339cfee633e4c8e0cf",
|
||||
"reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/context/zipball/3c414b246e0dabb7d6145404e6a5e4536ca18d07",
|
||||
"reference": "3c414b246e0dabb7d6145404e6a5e4536ca18d07",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1347,11 +1347,11 @@
|
||||
],
|
||||
"support": {
|
||||
"chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V",
|
||||
"docs": "https://opentelemetry.io/docs/php",
|
||||
"docs": "https://opentelemetry.io/docs/languages/php",
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2025-09-19T00:05:49+00:00"
|
||||
"time": "2025-10-19T06:44:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/exporter-otlp",
|
||||
@@ -1419,16 +1419,16 @@
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/gen-otlp-protobuf",
|
||||
"version": "1.8.0",
|
||||
"version": "1.9.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/gen-otlp-protobuf.git",
|
||||
"reference": "673af5b06545b513466081884b47ef15a536edde"
|
||||
"reference": "a229cf161d42001d64c8f21e8f678581fe1c66b9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/673af5b06545b513466081884b47ef15a536edde",
|
||||
"reference": "673af5b06545b513466081884b47ef15a536edde",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/a229cf161d42001d64c8f21e8f678581fe1c66b9",
|
||||
"reference": "a229cf161d42001d64c8f21e8f678581fe1c66b9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1474,30 +1474,30 @@
|
||||
],
|
||||
"support": {
|
||||
"chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V",
|
||||
"docs": "https://opentelemetry.io/docs/php",
|
||||
"docs": "https://opentelemetry.io/docs/languages/php",
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2025-09-17T23:10:12+00:00"
|
||||
"time": "2025-10-19T06:44:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/sdk",
|
||||
"version": "1.13.0",
|
||||
"version": "1.14.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/sdk.git",
|
||||
"reference": "c76f91203bf7ef98ab3f4e0a82ca21699af185e1"
|
||||
"reference": "6e3d0ce93e76555dd5e2f1d19443ff45b990e410"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/c76f91203bf7ef98ab3f4e0a82ca21699af185e1",
|
||||
"reference": "c76f91203bf7ef98ab3f4e0a82ca21699af185e1",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/6e3d0ce93e76555dd5e2f1d19443ff45b990e410",
|
||||
"reference": "6e3d0ce93e76555dd5e2f1d19443ff45b990e410",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"nyholm/psr7-server": "^1.1",
|
||||
"open-telemetry/api": "^1.7",
|
||||
"open-telemetry/api": "^1.8",
|
||||
"open-telemetry/context": "^1.4",
|
||||
"open-telemetry/sem-conv": "^1.0",
|
||||
"php": "^8.1",
|
||||
@@ -1575,7 +1575,7 @@
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2026-01-28T11:38:11+00:00"
|
||||
"time": "2026-03-21T11:50:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/sem-conv",
|
||||
@@ -1996,16 +1996,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpseclib/phpseclib",
|
||||
"version": "3.0.49",
|
||||
"version": "3.0.50",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpseclib/phpseclib.git",
|
||||
"reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9"
|
||||
"reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/6233a1e12584754e6b5daa69fe1289b47775c1b9",
|
||||
"reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9",
|
||||
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/aa6ad8321ed103dc3624fb600a25b66ebf78ec7b",
|
||||
"reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2086,7 +2086,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpseclib/phpseclib/issues",
|
||||
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.49"
|
||||
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.50"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2102,7 +2102,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-27T09:17:28+00:00"
|
||||
"time": "2026-03-19T02:57:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/clock",
|
||||
@@ -3403,16 +3403,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/agents",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/agents.git",
|
||||
"reference": "052227953678a30ecc4b5467401fcb0b2386471e"
|
||||
"reference": "06064fd9fb19b77ae45a12ec7bcbc17670912c30"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/agents/zipball/052227953678a30ecc4b5467401fcb0b2386471e",
|
||||
"reference": "052227953678a30ecc4b5467401fcb0b2386471e",
|
||||
"url": "https://api.github.com/repos/utopia-php/agents/zipball/06064fd9fb19b77ae45a12ec7bcbc17670912c30",
|
||||
"reference": "06064fd9fb19b77ae45a12ec7bcbc17670912c30",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3450,9 +3450,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/agents/issues",
|
||||
"source": "https://github.com/utopia-php/agents/tree/1.2.1"
|
||||
"source": "https://github.com/utopia-php/agents/tree/1.3.0"
|
||||
},
|
||||
"time": "2026-02-24T06:03:55+00:00"
|
||||
"time": "2026-03-26T03:51:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/analytics",
|
||||
@@ -3850,16 +3850,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/database",
|
||||
"version": "dev-fix-collection-recreate",
|
||||
"version": "5.3.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/database.git",
|
||||
"reference": "5208630969dfdfbe8eda9c34c6b28ce711ece7c4"
|
||||
"reference": "cff2b6ed63d3291b74110d086e16ff089fe05993"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/5208630969dfdfbe8eda9c34c6b28ce711ece7c4",
|
||||
"reference": "5208630969dfdfbe8eda9c34c6b28ce711ece7c4",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/cff2b6ed63d3291b74110d086e16ff089fe05993",
|
||||
"reference": "cff2b6ed63d3291b74110d086e16ff089fe05993",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3868,9 +3868,10 @@
|
||||
"ext-pdo": "*",
|
||||
"php": ">=8.4",
|
||||
"utopia-php/cache": "1.*",
|
||||
"utopia-php/framework": "0.33.*",
|
||||
"utopia-php/console": "0.1.*",
|
||||
"utopia-php/mongo": "1.*",
|
||||
"utopia-php/pools": "1.*"
|
||||
"utopia-php/pools": "1.*",
|
||||
"utopia-php/validators": "0.2.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "1.23.*",
|
||||
@@ -3880,7 +3881,7 @@
|
||||
"phpunit/phpunit": "9.*",
|
||||
"rregeer/phpunit-coverage-check": "0.3.*",
|
||||
"swoole/ide-helper": "5.1.3",
|
||||
"utopia-php/cli": "0.14.*"
|
||||
"utopia-php/cli": "0.22.*"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
@@ -3888,38 +3889,7 @@
|
||||
"Utopia\\Database\\": "src/Database"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\E2E\\": "tests/e2e",
|
||||
"Tests\\Unit\\": "tests/unit"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"docker compose build"
|
||||
],
|
||||
"start": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"docker compose up -d"
|
||||
],
|
||||
"test": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"docker compose exec tests vendor/bin/phpunit --configuration phpunit.xml"
|
||||
],
|
||||
"lint": [
|
||||
"php -d memory_limit=2G ./vendor/bin/pint --test"
|
||||
],
|
||||
"format": [
|
||||
"php -d memory_limit=2G ./vendor/bin/pint"
|
||||
],
|
||||
"check": [
|
||||
"./vendor/bin/phpstan analyse --level 7 src tests --memory-limit 2G"
|
||||
],
|
||||
"coverage": [
|
||||
"./vendor/bin/coverage-check ./tmp/clover.xml 90"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
@@ -3932,10 +3902,10 @@
|
||||
"utopia"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/utopia-php/database/tree/fix-collection-recreate",
|
||||
"issues": "https://github.com/utopia-php/database/issues"
|
||||
"issues": "https://github.com/utopia-php/database/issues",
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.17"
|
||||
},
|
||||
"time": "2026-03-16T11:58:09+00:00"
|
||||
"time": "2026-03-20T01:18:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/detector",
|
||||
@@ -4032,16 +4002,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/dns",
|
||||
"version": "1.6.5",
|
||||
"version": "1.6.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/dns.git",
|
||||
"reference": "574327f0f5fabefa7048030c5634cde33ad10640"
|
||||
"reference": "917901ecfe5f09a540e4f689b6cbb80b9f55035d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/dns/zipball/574327f0f5fabefa7048030c5634cde33ad10640",
|
||||
"reference": "574327f0f5fabefa7048030c5634cde33ad10640",
|
||||
"url": "https://api.github.com/repos/utopia-php/dns/zipball/917901ecfe5f09a540e4f689b6cbb80b9f55035d",
|
||||
"reference": "917901ecfe5f09a540e4f689b6cbb80b9f55035d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4083,9 +4053,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/dns/issues",
|
||||
"source": "https://github.com/utopia-php/dns/tree/1.6.5"
|
||||
"source": "https://github.com/utopia-php/dns/tree/1.6.6"
|
||||
},
|
||||
"time": "2026-02-19T16:06:46+00:00"
|
||||
"time": "2026-03-27T11:13:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/domains",
|
||||
@@ -4497,23 +4467,23 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/messaging",
|
||||
"version": "0.20.1",
|
||||
"version": "0.22.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/messaging.git",
|
||||
"reference": "fcb4c3c46a48008a677957690bd45ec934dd33b0"
|
||||
"reference": "a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/fcb4c3c46a48008a677957690bd45ec934dd33b0",
|
||||
"reference": "fcb4c3c46a48008a677957690bd45ec934dd33b0",
|
||||
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030",
|
||||
"reference": "a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"ext-openssl": "*",
|
||||
"giggsey/libphonenumber-for-php-lite": "9.0.23",
|
||||
"php": ">=8.0.0",
|
||||
"php": ">=8.1.0",
|
||||
"phpmailer/phpmailer": "6.9.1"
|
||||
},
|
||||
"require-dev": {
|
||||
@@ -4542,22 +4512,22 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/messaging/issues",
|
||||
"source": "https://github.com/utopia-php/messaging/tree/0.20.1"
|
||||
"source": "https://github.com/utopia-php/messaging/tree/0.22.0"
|
||||
},
|
||||
"time": "2026-02-06T09:56:06+00:00"
|
||||
"time": "2026-04-02T04:09:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/migration",
|
||||
"version": "1.7.0",
|
||||
"version": "1.9.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/migration.git",
|
||||
"reference": "97583ae502e40621ea91a71de19d053c5ae2e558"
|
||||
"reference": "7a86aeadf182b63a9f4ceba7e137588b31c5d2e2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/97583ae502e40621ea91a71de19d053c5ae2e558",
|
||||
"reference": "97583ae502e40621ea91a71de19d053c5ae2e558",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/7a86aeadf182b63a9f4ceba7e137588b31c5d2e2",
|
||||
"reference": "7a86aeadf182b63a9f4ceba7e137588b31c5d2e2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4597,22 +4567,22 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/migration/issues",
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.7.0"
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.9.1"
|
||||
},
|
||||
"time": "2026-03-10T06:36:27+00:00"
|
||||
"time": "2026-03-25T07:05:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/mongo",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/mongo.git",
|
||||
"reference": "83dbcde768d5fb40241f5ca8aa5ed8ca140a7469"
|
||||
"reference": "677a21c53f7a1316c528b4b45b3fce886cee7223"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/mongo/zipball/83dbcde768d5fb40241f5ca8aa5ed8ca140a7469",
|
||||
"reference": "83dbcde768d5fb40241f5ca8aa5ed8ca140a7469",
|
||||
"url": "https://api.github.com/repos/utopia-php/mongo/zipball/677a21c53f7a1316c528b4b45b3fce886cee7223",
|
||||
"reference": "677a21c53f7a1316c528b4b45b3fce886cee7223",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4658,9 +4628,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/mongo/issues",
|
||||
"source": "https://github.com/utopia-php/mongo/tree/1.0.1"
|
||||
"source": "https://github.com/utopia-php/mongo/tree/1.0.2"
|
||||
},
|
||||
"time": "2026-03-13T07:29:24+00:00"
|
||||
"time": "2026-03-18T02:45:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/platform",
|
||||
@@ -5246,22 +5216,23 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/vcs",
|
||||
"version": "2.0.2",
|
||||
"version": "3.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/vcs.git",
|
||||
"reference": "5769679308bad498f2777547d48ab332166c4c0b"
|
||||
"reference": "03b76ad5fd01bc50f809915bca6ff0745ea913af"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/5769679308bad498f2777547d48ab332166c4c0b",
|
||||
"reference": "5769679308bad498f2777547d48ab332166c4c0b",
|
||||
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/03b76ad5fd01bc50f809915bca6ff0745ea913af",
|
||||
"reference": "03b76ad5fd01bc50f809915bca6ff0745ea913af",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"adhocore/jwt": "^1.1",
|
||||
"php": ">=8.0",
|
||||
"utopia-php/cache": "1.0.*"
|
||||
"utopia-php/cache": "1.0.*",
|
||||
"utopia-php/fetch": "0.5.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.*.*",
|
||||
@@ -5288,9 +5259,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/vcs/issues",
|
||||
"source": "https://github.com/utopia-php/vcs/tree/2.0.2"
|
||||
"source": "https://github.com/utopia-php/vcs/tree/3.1.0"
|
||||
},
|
||||
"time": "2026-03-13T15:25:16+00:00"
|
||||
"time": "2026-03-24T08:49:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/websocket",
|
||||
@@ -5468,16 +5439,16 @@
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "1.11.6",
|
||||
"version": "1.14.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38"
|
||||
"reference": "7e7e257b10a8c1384a237e7d8d73452e2108901e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38",
|
||||
"reference": "f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/7e7e257b10a8c1384a237e7d8d73452e2108901e",
|
||||
"reference": "7e7e257b10a8c1384a237e7d8d73452e2108901e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5513,9 +5484,9 @@
|
||||
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/sdk-generator/issues",
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.11.6"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.14.0"
|
||||
},
|
||||
"time": "2026-03-09T07:12:51+00:00"
|
||||
"time": "2026-03-26T12:50:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brianium/paratest",
|
||||
@@ -6224,11 +6195,11 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.1.40",
|
||||
"version": "2.1.44",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b",
|
||||
"reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/4a88c083c668b2c364a425c9b3171b2d9ea5d218",
|
||||
"reference": "4a88c083c668b2c364a425c9b3171b2d9ea5d218",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6273,7 +6244,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-23T15:04:35+00:00"
|
||||
"time": "2026-03-25T17:34:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
@@ -8462,18 +8433,9 @@
|
||||
"time": "2024-11-07T12:36:22+00:00"
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
{
|
||||
"package": "utopia-php/database",
|
||||
"version": "dev-fix-collection-recreate",
|
||||
"alias": "5.3.15",
|
||||
"alias_normalized": "5.3.15.0"
|
||||
}
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "dev",
|
||||
"stability-flags": {
|
||||
"utopia-php/database": 20
|
||||
},
|
||||
"stability-flags": {},
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
@@ -8494,8 +8456,5 @@
|
||||
"platform-dev": {
|
||||
"ext-fileinfo": "*"
|
||||
},
|
||||
"platform-overrides": {
|
||||
"php": "8.3"
|
||||
},
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
|
||||
+56
-14
@@ -112,6 +112,8 @@ services:
|
||||
condition: service_healthy
|
||||
coredns:
|
||||
condition: service_started
|
||||
ollama:
|
||||
condition: service_started
|
||||
entrypoint:
|
||||
- php
|
||||
- -e
|
||||
@@ -159,6 +161,12 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER_VECTORSDB
|
||||
- _APP_DB_HOST_VECTORSDB
|
||||
- _APP_DB_PORT_VECTORSDB
|
||||
- _APP_DB_SCHEMA_VECTORSDB
|
||||
- _APP_DB_USER_VECTORSDB
|
||||
- _APP_DB_PASS_VECTORSDB
|
||||
- _APP_SMTP_HOST
|
||||
- _APP_SMTP_PORT
|
||||
- _APP_SMTP_SECURE
|
||||
@@ -246,7 +254,7 @@ services:
|
||||
appwrite-console:
|
||||
<<: *x-logging
|
||||
container_name: appwrite-console
|
||||
image: appwrite/console:7.5.7
|
||||
image: appwrite/console:7.8.26
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
@@ -295,6 +303,7 @@ services:
|
||||
depends_on:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- redis
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -311,6 +320,12 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER_VECTORSDB
|
||||
- _APP_DB_HOST_VECTORSDB
|
||||
- _APP_DB_PORT_VECTORSDB
|
||||
- _APP_DB_SCHEMA_VECTORSDB
|
||||
- _APP_DB_USER_VECTORSDB
|
||||
- _APP_DB_PASS_VECTORSDB
|
||||
- _APP_USAGE_STATS
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_LOGGING_CONFIG_REALTIME
|
||||
@@ -330,6 +345,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -363,6 +379,7 @@ services:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- request-catcher-sms
|
||||
- request-catcher-webhook
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -393,6 +410,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- ollama
|
||||
volumes:
|
||||
- appwrite-uploads:/storage/uploads:rw
|
||||
- appwrite-cache:/storage/cache:rw
|
||||
@@ -402,6 +420,7 @@ services:
|
||||
- appwrite-certificates:/storage/certificates:rw
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -458,9 +477,11 @@ services:
|
||||
volumes:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
|
||||
depends_on:
|
||||
- redis
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -476,6 +497,12 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER_VECTORSDB
|
||||
- _APP_DB_HOST_VECTORSDB
|
||||
- _APP_DB_PORT_VECTORSDB
|
||||
- _APP_DB_SCHEMA_VECTORSDB
|
||||
- _APP_DB_USER_VECTORSDB
|
||||
- _APP_DB_PASS_VECTORSDB
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_WORKERS_NUM
|
||||
- _APP_QUEUE_NAME
|
||||
@@ -497,6 +524,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -629,6 +657,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- ollama
|
||||
volumes:
|
||||
- appwrite-config:/storage/config:rw
|
||||
- appwrite-certificates:/storage/certificates:rw
|
||||
@@ -848,6 +877,7 @@ services:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
- ./tests:/usr/src/code/tests
|
||||
|
||||
depends_on:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
environment:
|
||||
@@ -1044,6 +1074,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -1077,6 +1108,7 @@ services:
|
||||
depends_on:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- redis
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -1107,6 +1139,7 @@ services:
|
||||
depends_on:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- redis
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -1137,6 +1170,7 @@ services:
|
||||
depends_on:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- redis
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -1228,7 +1262,6 @@ services:
|
||||
start_period: 5s
|
||||
|
||||
mariadb:
|
||||
profiles: ["mariadb"]
|
||||
image: mariadb:10.11 # fix issues when upgrading using: mysql_upgrade -u root -p
|
||||
container_name: appwrite-mariadb
|
||||
<<: *x-logging
|
||||
@@ -1252,7 +1285,6 @@ services:
|
||||
retries: 12
|
||||
|
||||
mongodb:
|
||||
profiles: ["mongodb"]
|
||||
image: mongo:8.2.5
|
||||
container_name: appwrite-mongodb
|
||||
<<: *x-logging
|
||||
@@ -1288,32 +1320,41 @@ services:
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
|
||||
|
||||
|
||||
postgresql:
|
||||
profiles: ["postgresql"]
|
||||
build:
|
||||
context: ./tests/resources/postgresql
|
||||
args:
|
||||
POSTGRES_VERSION: 17
|
||||
image: appwrite/postgres:0.1.0
|
||||
container_name: appwrite-postgresql
|
||||
<<: *x-logging
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- appwrite-postgresql:/var/lib/postgresql:rw
|
||||
- appwrite-postgresql:/var/lib/postgresql/18/data:rw
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
- POSTGRES_DB=${_APP_DB_SCHEMA}
|
||||
- POSTGRES_USER=${_APP_DB_USER}
|
||||
- POSTGRES_PASSWORD=${_APP_DB_PASS}
|
||||
command: "postgres"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER}"]
|
||||
test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER} -d ${_APP_DB_SCHEMA}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
command: "postgres"
|
||||
|
||||
ollama:
|
||||
image: appwrite/ollama:0.1.1
|
||||
container_name: ollama
|
||||
ports:
|
||||
- "11434:11434"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MODELS: ${_APP_EMBEDDING_MODELS:-embeddinggemma}
|
||||
OLLAMA_KEEP_ALIVE: 24h
|
||||
volumes:
|
||||
- appwrite-models:/root/.ollama
|
||||
networks:
|
||||
- appwrite
|
||||
|
||||
redis:
|
||||
image: redis:7.4.7-alpine
|
||||
@@ -1436,3 +1477,4 @@ volumes:
|
||||
appwrite-sites:
|
||||
appwrite-builds:
|
||||
appwrite-config:
|
||||
appwrite-models:
|
||||
@@ -0,0 +1 @@
|
||||
Create a new Collection. Before using this route, you should create a new database resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
|
||||
@@ -0,0 +1 @@
|
||||
Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
|
||||
@@ -0,0 +1 @@
|
||||
Create new Documents. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
|
||||
@@ -0,0 +1,2 @@
|
||||
Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.
|
||||
Attributes can be `key`, `fulltext`, and `unique`.
|
||||
@@ -0,0 +1 @@
|
||||
Create multiple operations in a single transaction.
|
||||
@@ -0,0 +1 @@
|
||||
Create a new transaction.
|
||||
@@ -0,0 +1 @@
|
||||
Create a new Database.
|
||||
@@ -0,0 +1 @@
|
||||
Decrement a specific column of a row by a given value.
|
||||
@@ -0,0 +1 @@
|
||||
Delete a collection by its unique ID. Only users with write permissions have access to delete this resource.
|
||||
@@ -0,0 +1 @@
|
||||
Delete a document by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Bulk delete documents using queries, if no queries are passed then all documents are deleted.
|
||||
@@ -0,0 +1 @@
|
||||
Delete an index.
|
||||
@@ -0,0 +1 @@
|
||||
Delete a transaction by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Delete a database by its unique ID. Only API keys with with databases.write scope can delete a database.
|
||||
@@ -0,0 +1 @@
|
||||
Get the collection activity logs list by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Get usage metrics and statistics for a collection. Returning the total number of documents. The response includes both current totals and historical data over time. 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, range defaults to 30 days.
|
||||
@@ -0,0 +1 @@
|
||||
Get a collection by its unique ID. This endpoint response returns a JSON object with the collection metadata.
|
||||
@@ -0,0 +1 @@
|
||||
Get usage metrics and statistics for a database. You can view the total number of collections, documents, and storage usage. The response includes both current totals and historical data over time. 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, range defaults to 30 days.
|
||||
@@ -0,0 +1 @@
|
||||
Get the document activity logs list by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Get a document by its unique ID. This endpoint response returns a JSON object with the document data.
|
||||
@@ -0,0 +1 @@
|
||||
Get index by ID.
|
||||
@@ -0,0 +1 @@
|
||||
Get the database activity logs list by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Get a transaction by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Get a database by its unique ID. This endpoint response returns a JSON object with the database metadata.
|
||||
@@ -0,0 +1 @@
|
||||
Increment a specific column of a row by a given value.
|
||||
@@ -0,0 +1 @@
|
||||
List attributes in the collection.
|
||||
@@ -0,0 +1 @@
|
||||
Get a list of all collections that belong to the provided databaseId. You can use the search parameter to filter your results.
|
||||
@@ -0,0 +1 @@
|
||||
Get a list of all the user's documents in a given collection. You can use the query params to filter your results.
|
||||
@@ -0,0 +1 @@
|
||||
List indexes in the collection.
|
||||
@@ -0,0 +1 @@
|
||||
List transactions across all databases.
|
||||
@@ -0,0 +1 @@
|
||||
List usage metrics and statistics for all databases in the project. You can view the total number of databases, collections, documents, and storage usage. The response includes both current totals and historical data over time. 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, range defaults to 30 days.
|
||||
@@ -0,0 +1 @@
|
||||
Get a list of all databases from the current Appwrite project. You can use the search parameter to filter your results.
|
||||
@@ -0,0 +1 @@
|
||||
Update a collection by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Update a document by its unique ID. Using the patch method you can pass only specific fields that will get updated.
|
||||
@@ -0,0 +1 @@
|
||||
Update all documents that match your queries, if no queries are submitted then all documents are updated. You can pass only specific fields to be updated.
|
||||
@@ -0,0 +1 @@
|
||||
Update a transaction, to either commit or roll back its operations.
|
||||
@@ -0,0 +1 @@
|
||||
Update a database by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Create or update a Document. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user