mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
541
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fc214afbe | ||
|
|
c768375e13 | ||
|
|
fcc5a747d1 | ||
|
|
78e4c0611b | ||
|
|
74a3d6531f | ||
|
|
45092a1049 | ||
|
|
868bba52aa | ||
|
|
0b4716721d | ||
|
|
2ffcb9b07e | ||
|
|
d91f6960f7 | ||
|
|
3542d360b0 | ||
|
|
9a821e250f | ||
|
|
d1715e3bf4 | ||
|
|
c8a3b709ee | ||
|
|
fed197a313 | ||
|
|
ec85ec9f2a | ||
|
|
1a59361200 | ||
|
|
658396b4a8 | ||
|
|
53438accce | ||
|
|
84f47dc1d2 | ||
|
|
1538f78c18 | ||
|
|
6acf6adf99 | ||
|
|
af3d6b2308 | ||
|
|
260928942b | ||
|
|
bbcb5e8a6e | ||
|
|
03d6eb7401 | ||
|
|
821e323a40 | ||
|
|
cab2233816 | ||
|
|
3a8c5a2e65 | ||
|
|
faeb9aa8d4 | ||
|
|
92dfe93033 | ||
|
|
3a524c24d9 | ||
|
|
b78725e650 | ||
|
|
e7a847a417 | ||
|
|
0ad44ceb94 | ||
|
|
9fe0a5a31b | ||
|
|
07f7271c51 | ||
|
|
d91a0e3048 | ||
|
|
ea484e593b | ||
|
|
ca1c83c2cf | ||
|
|
8c4473932f | ||
|
|
d5c24df28d | ||
|
|
141cdf4c82 | ||
|
|
f79a310965 | ||
|
|
65d99f8f90 | ||
|
|
c04f54da64 | ||
|
|
17888cc02d | ||
|
|
d2787c8555 | ||
|
|
42ca924557 | ||
|
|
a8098831a3 | ||
|
|
3f5e40ad46 | ||
|
|
10d882ce50 | ||
|
|
7a5d64a85f | ||
|
|
b03783c588 | ||
|
|
6567cff5dc | ||
|
|
db5c44c843 | ||
|
|
b2e1c162d2 | ||
|
|
e3c261b9e4 | ||
|
|
0dd2889de5 | ||
|
|
cc8705253e | ||
|
|
14e81c2e27 | ||
|
|
d60e7c641f | ||
|
|
f287e60e87 | ||
|
|
3f7d35065e | ||
|
|
abc67ab0c6 | ||
|
|
c2ae330888 | ||
|
|
1577be9172 | ||
|
|
505b481537 | ||
|
|
2ab6156db4 | ||
|
|
0f7c73c1ab | ||
|
|
928ae74ffe | ||
|
|
e4a86d9777 | ||
|
|
44a9aed48e | ||
|
|
ad78259c10 | ||
|
|
2c4a9e5ea9 | ||
|
|
3b1233eaa9 | ||
|
|
2a5df03c89 | ||
|
|
c3ddc6a05d | ||
|
|
db792999c1 | ||
|
|
ed4319314a | ||
|
|
2a5f21e982 | ||
|
|
7dbb1c6399 | ||
|
|
00db19257d | ||
|
|
f0a4cf78ab | ||
|
|
dae01f7077 | ||
|
|
26af9b7463 | ||
|
|
459db9b43e | ||
|
|
3476ab2627 | ||
|
|
cbf1085850 | ||
|
|
dac0e2a543 | ||
|
|
c076dfe6f1 | ||
|
|
f5aebe6673 | ||
|
|
70effd1e99 | ||
|
|
e7ace61273 | ||
|
|
6b6f04d2d3 | ||
|
|
be761b31c1 | ||
|
|
eb75b527ad | ||
|
|
d4ad369a7e | ||
|
|
eb7e2fd23b | ||
|
|
36eabfa2dd | ||
|
|
2634c928c7 | ||
|
|
e462d73f65 | ||
|
|
e2baefe8c6 | ||
|
|
4ffde8e827 | ||
|
|
57a695367b | ||
|
|
681688e252 | ||
|
|
a8718bccb5 | ||
|
|
eba03cb126 | ||
|
|
622bdb7b2b | ||
|
|
c8720805c7 | ||
|
|
fdb11af6bb | ||
|
|
b09386727b | ||
|
|
f3e8994bb8 | ||
|
|
fb68dcc89e | ||
|
|
5abcb86fd7 | ||
|
|
ff93957922 | ||
|
|
179a045399 | ||
|
|
d0985db27d | ||
|
|
3788d5bceb | ||
|
|
f785ffbd71 | ||
|
|
c1fb4ecf39 | ||
|
|
e6a97bb36a | ||
|
|
a1539395c6 | ||
|
|
94468aa28f | ||
|
|
72ee7d7f34 | ||
|
|
8d7be399d3 | ||
|
|
f42a78ef9c | ||
|
|
5842f1950a | ||
|
|
ba4f708137 | ||
|
|
2fdd0656d0 | ||
|
|
790cdc297b | ||
|
|
990698e423 | ||
|
|
632f78140f | ||
|
|
243ddd4b33 | ||
|
|
93a99b561c | ||
|
|
6a5defb91c | ||
|
|
2f93dcc4df | ||
|
|
603b8ebd8c | ||
|
|
4dee937e36 | ||
|
|
bb92074093 | ||
|
|
fb127270ce | ||
|
|
11ceeff5ca | ||
|
|
8ed5489d9c | ||
|
|
1fefb7e133 | ||
|
|
6a6c3445fa | ||
|
|
6fe725ff9d | ||
|
|
4115537f72 | ||
|
|
26b2c27eb0 | ||
|
|
62bc540db0 | ||
|
|
1ef5c9bfcd | ||
|
|
b07bdf610a | ||
|
|
8d7705a2e3 | ||
|
|
bfe042dc3e | ||
|
|
5518ca165e | ||
|
|
13a879c274 | ||
|
|
7b5be4e154 | ||
|
|
3c7fae7843 | ||
|
|
742e80c0c6 | ||
|
|
738a696ca9 | ||
|
|
99c78738f5 | ||
|
|
c192e48c48 | ||
|
|
7acdaa5978 | ||
|
|
a755d76f52 | ||
|
|
bc88197e0d | ||
|
|
1e966d6b2e | ||
|
|
77c4c5d294 | ||
|
|
2ff7c5ac8e | ||
|
|
7db740d9ea | ||
|
|
0a518cd47e | ||
|
|
dc6f9e6d59 | ||
|
|
47fc6efb0d | ||
|
|
0ff6e99354 | ||
|
|
af21b44125 | ||
|
|
62246b5a2c | ||
|
|
5ff8fd98a7 | ||
|
|
d849aa1ed2 | ||
|
|
a3b33ab746 | ||
|
|
537bbdf419 | ||
|
|
81f6e1990a | ||
|
|
99bf0b8ff7 | ||
|
|
94c423c429 | ||
|
|
6c09d04038 | ||
|
|
44ed6826a9 | ||
|
|
36fddd4156 | ||
|
|
7a2ee683e3 | ||
|
|
359b3326d9 | ||
|
|
7a94bd143c | ||
|
|
9b7a5f55b7 | ||
|
|
e5dfed0aa3 | ||
|
|
215a139e87 | ||
|
|
5d2d74e64d | ||
|
|
906e227359 | ||
|
|
91e625cfc2 | ||
|
|
126c3a38de | ||
|
|
6ceeacbe03 | ||
|
|
186772cc15 | ||
|
|
7e444b037d | ||
|
|
6d15ffa3ca | ||
|
|
304b72a0c8 | ||
|
|
ecb48b389e | ||
|
|
c0ae2e6549 | ||
|
|
4c96f76762 | ||
|
|
216a5d7001 | ||
|
|
772d42cef8 | ||
|
|
e2c05fea8c | ||
|
|
73b1837d66 | ||
|
|
50c2ebc950 | ||
|
|
b024cb5614 | ||
|
|
47af19dd17 | ||
|
|
2b31c76918 | ||
|
|
26d8278a89 | ||
|
|
6997eec9dd | ||
|
|
192599d7c6 | ||
|
|
57c019ff98 | ||
|
|
867bc0281c | ||
|
|
dc2107fe66 | ||
|
|
ab2d830cc0 | ||
|
|
d3e9d03329 | ||
|
|
351abe50d3 | ||
|
|
f4a4cfa207 | ||
|
|
47ea083ea4 | ||
|
|
64fde6cddd | ||
|
|
5bd106afaf | ||
|
|
448b3b0cc0 | ||
|
|
f3579b3f12 | ||
|
|
09a7fc9063 | ||
|
|
2a65dfcc5d | ||
|
|
d220c2bdd7 | ||
|
|
e9a13fe93c | ||
|
|
2b865bdf97 | ||
|
|
19b1de283b | ||
|
|
d04f9f41c3 | ||
|
|
ff7523d6db | ||
|
|
9fc1b0b71b | ||
|
|
0e30f6533e | ||
|
|
c116de29e1 | ||
|
|
370704fafd | ||
|
|
1e247bdde1 | ||
|
|
6629fdd87f | ||
|
|
37316f97ff | ||
|
|
d3b8ce704e | ||
|
|
a0b38a5a7c | ||
|
|
554bc89cb5 | ||
|
|
b08e3d4e7b | ||
|
|
90cf28389c | ||
|
|
8581df9c14 | ||
|
|
de96e0f3dc | ||
|
|
6bd810cb3a | ||
|
|
0d106684ae | ||
|
|
e5245909e3 | ||
|
|
3f63b87ea6 | ||
|
|
c1ca623bab | ||
|
|
5d164e0592 | ||
|
|
a954cea8fc | ||
|
|
f9d6da6672 | ||
|
|
40e17ad625 | ||
|
|
da014b0ad3 | ||
|
|
be3e3baca0 | ||
|
|
4cb48638be | ||
|
|
78f7b1897e | ||
|
|
bd920ead40 | ||
|
|
310d75b6be | ||
|
|
a2d97b3b22 | ||
|
|
65c90c9475 | ||
|
|
d344ba46c4 | ||
|
|
b403a13061 | ||
|
|
b0a8aac0b7 | ||
|
|
dea3c0416b | ||
|
|
44182b4444 | ||
|
|
fc2a4e52e2 | ||
|
|
747779b245 | ||
|
|
a7048255e8 | ||
|
|
ef72e08386 | ||
|
|
864703fb4e | ||
|
|
472712dbb7 | ||
|
|
dcc0aa6ea5 | ||
|
|
deb189f44b | ||
|
|
8163eefe4d | ||
|
|
9ba2eafa83 | ||
|
|
03f2041913 | ||
|
|
fae28b468c | ||
|
|
d9c04f4fc0 | ||
|
|
e5f0e00742 | ||
|
|
0a2be34ceb | ||
|
|
ba32170f72 | ||
|
|
4199126984 | ||
|
|
01d76a1746 | ||
|
|
fbd277f6aa | ||
|
|
99c163d217 | ||
|
|
0c3ed13ad3 | ||
|
|
d2d36bbfae | ||
|
|
80f5ded0e3 | ||
|
|
6b1330e400 | ||
|
|
b6b5570b0b | ||
|
|
faf39fcc81 | ||
|
|
06e385b346 | ||
|
|
127aa3cea7 | ||
|
|
0a17e2bce9 | ||
|
|
a29cef2334 | ||
|
|
7a2f7f5a17 | ||
|
|
671b1624d5 | ||
|
|
f14250bf2a | ||
|
|
99f1543f1b | ||
|
|
3236e9eabe | ||
|
|
879320e23e | ||
|
|
b2b863f09f | ||
|
|
d86ee91316 | ||
|
|
a2ad870084 | ||
|
|
d070989f73 | ||
|
|
c99976560d | ||
|
|
f5705df02c | ||
|
|
4f7b1748f4 | ||
|
|
cc1272f80b | ||
|
|
9b2fbc5c52 | ||
|
|
88a9fc380e | ||
|
|
3d1b61d20d | ||
|
|
4ffeb866ca | ||
|
|
643f5e845a | ||
|
|
1c236959ba | ||
|
|
bf0f1ee12c | ||
|
|
9cb5eb0180 | ||
|
|
a28be2bf48 | ||
|
|
5455db7c7f | ||
|
|
260b280e10 | ||
|
|
86ce7a3004 | ||
|
|
83a009dcba | ||
|
|
94a18ede78 | ||
|
|
e8ff828039 | ||
|
|
0fdc28e034 | ||
|
|
d9b300051a | ||
|
|
f1ba7b08ab | ||
|
|
3bde473ff0 | ||
|
|
9889048429 | ||
|
|
18ab5ca28f | ||
|
|
77d423e645 | ||
|
|
9382e4cec5 | ||
|
|
d1f0fddbe7 | ||
|
|
4a837907d1 | ||
|
|
b398338876 | ||
|
|
cbd3e85b38 | ||
|
|
a6b4ade39b | ||
|
|
b6b1b396b3 | ||
|
|
26ac88c32e | ||
|
|
a8af6fde8d | ||
|
|
ddb7a403db | ||
|
|
80315a02dd | ||
|
|
d1751565cf | ||
|
|
f1e066d965 | ||
|
|
585b904b80 | ||
|
|
e1ebf153cb | ||
|
|
d3143b8ede | ||
|
|
394f28146a | ||
|
|
1857c0e04d | ||
|
|
0ae5712bb6 | ||
|
|
1eb59daed1 | ||
|
|
bf92ae6841 | ||
|
|
276a12e0f7 | ||
|
|
9ee949104f | ||
|
|
ffa4bfd1f6 | ||
|
|
fac204b34e | ||
|
|
042c8f8f47 | ||
|
|
aa9e9aa1ed | ||
|
|
6303029f8a | ||
|
|
45df2d3058 | ||
|
|
099308fef5 | ||
|
|
ba44e06af6 | ||
|
|
a47dcac424 | ||
|
|
24d3aa499a | ||
|
|
4e24e3cdc2 | ||
|
|
eff578bb36 | ||
|
|
9a2f97b613 | ||
|
|
5cd94041dc | ||
|
|
9c510dace8 | ||
|
|
56a374e0af | ||
|
|
ccfd856e8e | ||
|
|
0529cffa4b | ||
|
|
1023f24d0b | ||
|
|
bfbcecd069 | ||
|
|
05f8f8fb9d | ||
|
|
457e47cdd7 | ||
|
|
683fccc080 | ||
|
|
42246c5174 | ||
|
|
b3729507de | ||
|
|
b301a58b3e | ||
|
|
9f1b356a79 | ||
|
|
76e05983ff | ||
|
|
e41194ee3c | ||
|
|
6d17f8e877 | ||
|
|
0f0d43298e | ||
|
|
6b6946e39b | ||
|
|
92a307c164 | ||
|
|
e56b30e6a6 | ||
|
|
d1850fca38 | ||
|
|
f334168f90 | ||
|
|
6d357d617d | ||
|
|
e605beaff2 | ||
|
|
4f1d8ed6f4 | ||
|
|
951a749b50 | ||
|
|
c21a063642 | ||
|
|
28d00668bd | ||
|
|
aeeb7e1d55 | ||
|
|
75e1779921 | ||
|
|
72a2a043cd | ||
|
|
aa7d1a87f4 | ||
|
|
731e7b86a7 | ||
|
|
8b5783d3ff | ||
|
|
ab84fa8a4d | ||
|
|
9dca3c4300 | ||
|
|
5606402fa3 | ||
|
|
5d121c1887 | ||
|
|
1e9ee66231 | ||
|
|
eb61d5c25c | ||
|
|
f5c2c8d8f7 | ||
|
|
4710310629 | ||
|
|
610b9518fb | ||
|
|
c1af9f4201 | ||
|
|
4245c0b86e | ||
|
|
6b0586eaf6 | ||
|
|
018b7c38bd | ||
|
|
c60524e11f | ||
|
|
ea3b9ca0ec | ||
|
|
14e2488f98 | ||
|
|
aae672d3be | ||
|
|
298d6ea917 | ||
|
|
fa7f9be6f7 | ||
|
|
1595d41490 | ||
|
|
38a3c08772 | ||
|
|
f2d20cc5c2 | ||
|
|
f127ca651f | ||
|
|
4e7b61b6c8 | ||
|
|
d79411813a | ||
|
|
4d0242b48b | ||
|
|
eb87b951c6 | ||
|
|
fa6f9acaed | ||
|
|
65b1e1e948 | ||
|
|
62808bfb8b | ||
|
|
8218e95f17 | ||
|
|
b6fabf80ef | ||
|
|
11a9583c72 | ||
|
|
49bb3444bf | ||
|
|
e382a9d58b | ||
|
|
d4ff696173 | ||
|
|
0382b2250a | ||
|
|
799fe2acca | ||
|
|
f3544485e5 | ||
|
|
4b7676158e | ||
|
|
5cb818066f | ||
|
|
d34050a5df | ||
|
|
8bec64b2a2 | ||
|
|
7e12da5a2a | ||
|
|
e109f1e008 | ||
|
|
4f539f1ed4 | ||
|
|
dea3e74b6a | ||
|
|
7104f9e2c8 | ||
|
|
632c902e1c | ||
|
|
c1f10b06a3 | ||
|
|
c4806e883e | ||
|
|
2c71de1d1b | ||
|
|
2e89113e6a | ||
|
|
edee5400ba | ||
|
|
82dfe5ddee | ||
|
|
0885f31dd7 | ||
|
|
9472bff8f9 | ||
|
|
68548ebd47 | ||
|
|
b9ac8e644d | ||
|
|
7b8e37b03d | ||
|
|
9960912bb1 | ||
|
|
1869852985 | ||
|
|
35163ff52a | ||
|
|
f44543f083 | ||
|
|
cefc1b6519 | ||
|
|
eb5fc0797f | ||
|
|
464b913e90 | ||
|
|
336afc5ca2 | ||
|
|
33f92d9ab3 | ||
|
|
61e2b7a787 | ||
|
|
5a3fd99a28 | ||
|
|
eefbc68f6b | ||
|
|
7791cdc201 | ||
|
|
daec743e8c | ||
|
|
c2ce7d0aa1 | ||
|
|
9eea574df5 | ||
|
|
da504ba46c | ||
|
|
c2c645188d | ||
|
|
7d23b0af5f | ||
|
|
0853de0760 | ||
|
|
b2e4513af3 | ||
|
|
a26c9214cb | ||
|
|
d3b91b5ec5 | ||
|
|
a5ea32c237 | ||
|
|
88efb36ead | ||
|
|
902f20f20c | ||
|
|
f6281158a2 | ||
|
|
154e972217 | ||
|
|
0c2374e66a | ||
|
|
15863c2886 | ||
|
|
618c14524d | ||
|
|
6c4983f65f | ||
|
|
ea282a1761 | ||
|
|
8fed930b7d | ||
|
|
a66fc13ebc | ||
|
|
8d7ab130ff | ||
|
|
569d54bee3 | ||
|
|
5138d4c0f9 | ||
|
|
d37bfa0a5e | ||
|
|
2cce14418c | ||
|
|
e6b0d22e07 | ||
|
|
ba8cafbd45 | ||
|
|
e920c1f8b4 | ||
|
|
8ac7aca2a4 | ||
|
|
ec17284bfd | ||
|
|
2d123f60e1 | ||
|
|
7721f29d0e | ||
|
|
af87c24cfd | ||
|
|
4b7c13d3e2 | ||
|
|
bb9dd4423d | ||
|
|
303212fba8 | ||
|
|
acc408f5ab | ||
|
|
086116d0f0 | ||
|
|
e93681afc0 | ||
|
|
7d2b4503e3 | ||
|
|
0d314df561 | ||
|
|
006d4237f5 | ||
|
|
2c3b860e63 | ||
|
|
613a733169 | ||
|
|
0587aec643 | ||
|
|
b334f2e345 | ||
|
|
777c8dd587 | ||
|
|
4be8be1353 | ||
|
|
8cc1c0d6dc | ||
|
|
eaba2eb7e5 | ||
|
|
9ccaa5a883 | ||
|
|
93fa7496b1 | ||
|
|
45f16e560c | ||
|
|
ec93239630 | ||
|
|
2f7b0c9938 | ||
|
|
ef12b75013 | ||
|
|
bed88baa6c | ||
|
|
9562b95285 | ||
|
|
e9fda6168c | ||
|
|
a6768bdc32 |
@@ -1,15 +1,17 @@
|
||||
_APP_ENV=development
|
||||
_APP_EDITION=self-hosted
|
||||
_APP_LOCALE=en
|
||||
_APP_WORKER_PER_CORE=6
|
||||
_APP_CONSOLE_WHITELIST_ROOT=disabled
|
||||
_APP_CONSOLE_WHITELIST_EMAILS=
|
||||
_APP_CONSOLE_WHITELIST_IPS=
|
||||
_APP_CONSOLE_HOSTNAMES=localhost,appwrite.io,*.appwrite.io
|
||||
_APP_SYSTEM_EMAIL_NAME=Appwrite
|
||||
_APP_SYSTEM_EMAIL_ADDRESS=team@appwrite.io
|
||||
_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=security@appwrite.io
|
||||
_APP_SYSTEM_RESPONSE_FORMAT=
|
||||
_APP_OPTIONS_ABUSE=disabled
|
||||
_APP_OPTIONS_ROUTER_PROTECTION=disbled
|
||||
_APP_OPTIONS_ROUTER_PROTECTION=disabled
|
||||
_APP_OPTIONS_FORCE_HTTPS=disabled
|
||||
_APP_OPTIONS_FUNCTIONS_FORCE_HTTPS=disabled
|
||||
_APP_OPENSSL_KEY_V1=your-secret-key
|
||||
@@ -57,6 +59,7 @@ _APP_SMTP_USERNAME=
|
||||
_APP_SMTP_PASSWORD=
|
||||
_APP_SMS_PROVIDER=sms://username:password@mock
|
||||
_APP_SMS_FROM=+123456789
|
||||
_APP_SMS_PROJECTS_DENY_LIST=
|
||||
_APP_STORAGE_LIMIT=30000000
|
||||
_APP_STORAGE_PREVIEW_LIMIT=20000000
|
||||
_APP_FUNCTIONS_SIZE_LIMIT=30000000
|
||||
@@ -71,11 +74,12 @@ _APP_EXECUTOR_SECRET=your-secret-key
|
||||
_APP_EXECUTOR_HOST=http://proxy/v1
|
||||
_APP_FUNCTIONS_RUNTIMES=php-8.0,node-18.0,python-3.9,ruby-3.1
|
||||
_APP_MAINTENANCE_INTERVAL=86400
|
||||
_APP_MAINTENANCE_DELAY=
|
||||
_APP_MAINTENANCE_RETENTION_CACHE=2592000
|
||||
_APP_MAINTENANCE_RETENTION_EXECUTION=1209600
|
||||
_APP_MAINTENANCE_RETENTION_ABUSE=86400
|
||||
_APP_MAINTENANCE_RETENTION_AUDIT=1209600
|
||||
_APP_USAGE_AGGREGATION_INTERVAL=5
|
||||
_APP_USAGE_AGGREGATION_INTERVAL=20
|
||||
_APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000
|
||||
_APP_MAINTENANCE_RETENTION_SCHEDULES=86400
|
||||
_APP_USAGE_STATS=enabled
|
||||
|
||||
@@ -17,6 +17,12 @@ jobs:
|
||||
fetch-depth: 2
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
@@ -35,11 +41,11 @@ jobs:
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
VERSION=${{ steps.meta.outputs.version }}
|
||||
VITE_APPWRITE_GROWTH_ENDPOINT=https://growth.appwrite.io/v1
|
||||
VITE_GA_PROJECT=G-L7G2B6PLDS
|
||||
VITE_CONSOLE_MODE=cloud
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
@@ -75,8 +75,32 @@ jobs:
|
||||
- name: Run Unit Tests
|
||||
run: docker compose exec appwrite test /usr/src/code/tests/unit
|
||||
|
||||
e2e_test:
|
||||
name: E2E Test
|
||||
e2e_general_test:
|
||||
name: E2E General Test
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose up -d
|
||||
sleep 10
|
||||
|
||||
- name: Run General Tests
|
||||
run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/General --debug
|
||||
|
||||
e2e_service_test:
|
||||
name: E2E Service Test
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
strategy:
|
||||
@@ -119,4 +143,4 @@ jobs:
|
||||
sleep 10
|
||||
|
||||
- name: Run ${{matrix.service}} Tests
|
||||
run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug
|
||||
run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug
|
||||
|
||||
@@ -13,3 +13,4 @@ debug/
|
||||
app/sdks
|
||||
dev/yasd_init.php
|
||||
.phpunit.result.cache
|
||||
appwrite.json
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
[submodule "app/console"]
|
||||
path = app/console
|
||||
url = https://github.com/appwrite/console
|
||||
branch = billing
|
||||
branch = 3.2.16
|
||||
|
||||
+8
-1
@@ -84,6 +84,10 @@ RUN chmod +x /usr/local/bin/doctor && \
|
||||
chmod +x /usr/local/bin/ssl && \
|
||||
chmod +x /usr/local/bin/test && \
|
||||
chmod +x /usr/local/bin/vars && \
|
||||
chmod +x /usr/local/bin/queue-retry && \
|
||||
chmod +x /usr/local/bin/queue-count-failed && \
|
||||
chmod +x /usr/local/bin/queue-count-processing && \
|
||||
chmod +x /usr/local/bin/queue-count-success && \
|
||||
chmod +x /usr/local/bin/worker-audits && \
|
||||
chmod +x /usr/local/bin/worker-certificates && \
|
||||
chmod +x /usr/local/bin/worker-databases && \
|
||||
@@ -93,8 +97,11 @@ RUN chmod +x /usr/local/bin/doctor && \
|
||||
chmod +x /usr/local/bin/worker-mails && \
|
||||
chmod +x /usr/local/bin/worker-messaging && \
|
||||
chmod +x /usr/local/bin/worker-webhooks && \
|
||||
chmod +x /usr/local/bin/worker-migrations && \
|
||||
chmod +x /usr/local/bin/worker-hamster && \
|
||||
chmod +x /usr/local/bin/worker-usage && \
|
||||
chmod +x /usr/local/bin/worker-migrations
|
||||
chmod +x /usr/local/bin/worker-usage-dump
|
||||
|
||||
|
||||
# Cloud Executabless
|
||||
RUN chmod +x /usr/local/bin/hamster && \
|
||||
|
||||
+31
-3
@@ -6,6 +6,7 @@ require_once __DIR__ . '/controllers/general.php';
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Hamster;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
use Utopia\CLI\CLI;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
@@ -24,7 +25,7 @@ use Utopia\Registry\Registry;
|
||||
|
||||
Authorization::disable();
|
||||
|
||||
CLI::setResource('register', fn()=>$register);
|
||||
CLI::setResource('register', fn() => $register);
|
||||
|
||||
CLI::setResource('cache', function ($pools) {
|
||||
$list = Config::getParam('pools-cache', []);
|
||||
@@ -71,7 +72,7 @@ CLI::setResource('dbForConsole', function ($pools, $cache) {
|
||||
$collections = Config::getParam('collections', [])['console'];
|
||||
$last = \array_key_last($collections);
|
||||
|
||||
if (!($dbForConsole->exists($dbForConsole->getDefaultDatabase(), $last))) { /** TODO cache ready variable using registry */
|
||||
if (!($dbForConsole->exists($dbForConsole->getDatabase(), $last))) { /** TODO cache ready variable using registry */
|
||||
throw new Exception('Tables not ready yet.');
|
||||
}
|
||||
|
||||
@@ -102,7 +103,19 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole,
|
||||
|
||||
if (isset($databases[$databaseName])) {
|
||||
$database = $databases[$databaseName];
|
||||
$database->setNamespace('_' . $project->getInternalId());
|
||||
|
||||
if ($project->getAttribute('database') === DATABASE_SHARED_TABLES) {
|
||||
$database
|
||||
->setShareTables(true)
|
||||
->setTenant($project->getInternalId())
|
||||
->setNamespace('');
|
||||
} else {
|
||||
$database
|
||||
->setShareTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getInternalId());
|
||||
}
|
||||
|
||||
return $database;
|
||||
}
|
||||
|
||||
@@ -115,6 +128,18 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole,
|
||||
|
||||
$databases[$databaseName] = $database;
|
||||
|
||||
if ($project->getAttribute('database') === DATABASE_SHARED_TABLES) {
|
||||
$database
|
||||
->setShareTables(true)
|
||||
->setTenant($project->getInternalId())
|
||||
->setNamespace('');
|
||||
} else {
|
||||
$database
|
||||
->setShareTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getInternalId());
|
||||
}
|
||||
|
||||
$database
|
||||
->setNamespace('_' . $project->getInternalId())
|
||||
->setMetadata('host', \gethostname())
|
||||
@@ -130,6 +155,9 @@ CLI::setResource('queue', function (Group $pools) {
|
||||
CLI::setResource('queueForFunctions', function (Connection $queue) {
|
||||
return new Func($queue);
|
||||
}, ['queue']);
|
||||
CLI::setResource('queueForHamster', function (Connection $queue) {
|
||||
return new Hamster($queue);
|
||||
}, ['queue']);
|
||||
CLI::setResource('queueForDeletes', function (Connection $queue) {
|
||||
return new Delete($queue);
|
||||
}, ['queue']);
|
||||
|
||||
+57
-57
@@ -18,6 +18,63 @@ $auth = Config::getParam('auth', []);
|
||||
*/
|
||||
|
||||
$commonCollections = [
|
||||
'cache' => [
|
||||
'$collection' => Database::METADATA,
|
||||
'$id' => 'cache',
|
||||
'name' => 'Cache',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => 'resource',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 255,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'accessedAt',
|
||||
'type' => Database::VAR_DATETIME,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => ['datetime'],
|
||||
],
|
||||
[
|
||||
'$id' => 'signature',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 255,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => '_key_accessedAt',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['accessedAt'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
[
|
||||
'$id' => '_key_resource',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['resource'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'users' => [
|
||||
'$collection' => ID::custom(Database::METADATA),
|
||||
'$id' => ID::custom('users'),
|
||||
@@ -2872,63 +2929,6 @@ $projectCollections = array_merge([
|
||||
],
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
'$collection' => Database::METADATA,
|
||||
'$id' => 'cache',
|
||||
'name' => 'Cache',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => 'resource',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 255,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'accessedAt',
|
||||
'type' => Database::VAR_DATETIME,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => ['datetime'],
|
||||
],
|
||||
[
|
||||
'$id' => 'signature',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 255,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => '_key_accessedAt',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['accessedAt'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
[
|
||||
'$id' => '_key_resource',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['resource'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'variables' => [
|
||||
'$collection' => Database::METADATA,
|
||||
'$id' => 'variables',
|
||||
|
||||
+26
-3
@@ -31,7 +31,7 @@ return [
|
||||
Exception::GENERAL_SERVICE_DISABLED => [
|
||||
'name' => Exception::GENERAL_SERVICE_DISABLED,
|
||||
'description' => 'The requested service is disabled. You can enable the service from the Appwrite console.',
|
||||
'code' => 503,
|
||||
'code' => 403,
|
||||
],
|
||||
Exception::GENERAL_UNAUTHORIZED_SCOPE => [
|
||||
'name' => Exception::GENERAL_UNAUTHORIZED_SCOPE,
|
||||
@@ -103,6 +103,11 @@ return [
|
||||
'description' => 'This method was not fully implemented yet. If you believe this is a mistake, please upgrade your Appwrite server version.',
|
||||
'code' => 405,
|
||||
],
|
||||
Exception::GENERAL_BAD_REQUEST => [
|
||||
'name' => Exception::GENERAL_BAD_REQUEST,
|
||||
'description' => 'There was an error processing your request. Please check the inputs and try again.',
|
||||
'code' => 400,
|
||||
],
|
||||
|
||||
/** User Errors */
|
||||
Exception::USER_COUNT_EXCEEDED => [
|
||||
@@ -251,6 +256,11 @@ return [
|
||||
'description' => 'User phone is already verified',
|
||||
'code' => 409
|
||||
],
|
||||
Exception::USER_DELETION_PROHIBITED => [
|
||||
'name' => Exception::USER_DELETION_PROHIBITED,
|
||||
'description' => 'User deletion is not allowed for users with active memberships. Please delete all confirmed memberships before deleting the account.',
|
||||
'code' => 400
|
||||
],
|
||||
|
||||
/** Teams */
|
||||
Exception::TEAM_NOT_FOUND => [
|
||||
@@ -680,6 +690,7 @@ return [
|
||||
'name' => Exception::RULE_VERIFICATION_FAILED,
|
||||
'description' => 'Domain verification failed. Please check if your DNS records are correct and try again.',
|
||||
'code' => 401,
|
||||
'publish' => true
|
||||
],
|
||||
Exception::PROJECT_SMTP_CONFIG_INVALID => [
|
||||
'name' => Exception::PROJECT_SMTP_CONFIG_INVALID,
|
||||
@@ -767,10 +778,22 @@ return [
|
||||
],
|
||||
|
||||
/** Health */
|
||||
Exception::QUEUE_SIZE_EXCEEDED => [
|
||||
'name' => Exception::QUEUE_SIZE_EXCEEDED,
|
||||
Exception::HEALTH_QUEUE_SIZE_EXCEEDED => [
|
||||
'name' => Exception::HEALTH_QUEUE_SIZE_EXCEEDED,
|
||||
'description' => 'Queue size threshold hit.',
|
||||
'code' => 503,
|
||||
'publish' => false
|
||||
],
|
||||
|
||||
Exception::HEALTH_CERTIFICATE_EXPIRED => [
|
||||
'name' => Exception::HEALTH_CERTIFICATE_EXPIRED,
|
||||
'description' => 'The SSL certificate for the specified domain has expired and is no longer valid.',
|
||||
'code' => 404,
|
||||
],
|
||||
|
||||
Exception::HEALTH_INVALID_HOST => [
|
||||
'name' => Exception::HEALTH_INVALID_HOST,
|
||||
'description' => 'Failed to establish a connection to the specified domain. Please verify the domain name and ensure that the server is running and accessible.',
|
||||
'code' => 404,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -3,30 +3,36 @@
|
||||
"settings.locale": "tr",
|
||||
"settings.direction": "ltr",
|
||||
"emails.sender": "%s Takımı",
|
||||
"emails.verification.subject": "",
|
||||
"emails.verification.hello": "",
|
||||
"emails.verification.body": "",
|
||||
"emails.verification.footer": "",
|
||||
"emails.verification.thanks": "",
|
||||
"emails.verification.signature": "",
|
||||
"emails.magicSession.subject": "",
|
||||
"emails.magicSession.hello": "",
|
||||
"emails.magicSession.body": "",
|
||||
"emails.magicSession.footer": "",
|
||||
"emails.magicSession.thanks": "",
|
||||
"emails.magicSession.signature": "",
|
||||
"emails.recovery.subject": "",
|
||||
"emails.recovery.hello": "",
|
||||
"emails.recovery.body": "",
|
||||
"emails.recovery.footer": "",
|
||||
"emails.recovery.thanks": "",
|
||||
"emails.recovery.signature": "",
|
||||
"emails.invitation.subject": "",
|
||||
"emails.invitation.hello": "",
|
||||
"emails.invitation.body": "",
|
||||
"emails.invitation.footer": "",
|
||||
"emails.invitation.thanks": "",
|
||||
"emails.invitation.signature": "",
|
||||
"emails.verification.subject": "Hesabını Doğrula",
|
||||
"emails.verification.hello": "Merhaba {{user}}",
|
||||
"emails.verification.body": "Eposta adresini doğrulamak için bu bağlantıyı kullanın.",
|
||||
"emails.verification.footer": "Eğer bu eposta adresini doğrulamak isteyen siz değilseniz devam etmeyin.",
|
||||
"emails.verification.thanks": "Teşekkürler",
|
||||
"emails.verification.signature": "{{project}} takımı",
|
||||
"emails.magicSession.subject": "Giriş",
|
||||
"emails.magicSession.hello": "Merhaba,",
|
||||
"emails.magicSession.body": "Giriş yapmak için tıklayın.",
|
||||
"emails.magicSession.footer": "Eğer bu eposta adresini kullanarak giriş yapmak istemediyseniz devam etmeyin.",
|
||||
"emails.magicSession.thanks": "Teşekkürler",
|
||||
"emails.magicSession.signature": "{{project}} takımı",
|
||||
"emails.recovery.subject": "Şifremi Sıfırla",
|
||||
"emails.recovery.hello": "Merhaba {{user}}",
|
||||
"emails.recovery.body": "{{project}} şifrenizi sıfırlamak için bu bağlantıyı kullanın.",
|
||||
"emails.recovery.footer": "Eğer şifre sıfırlama talebinde bulunmadıysanız devam etmeyin.",
|
||||
"emails.recovery.thanks": "Teşekkürler",
|
||||
"emails.recovery.signature": "{{project}} takımı",
|
||||
"emails.invitation.subject": "%s üzerinde %s Takımına Davet",
|
||||
"emails.invitation.hello": "Merhaba",
|
||||
"emails.invitation.body": "Bu epostayı aldınız, çünkü {{owner}} sizi {{project}} üzerinde {{team}} takımının üyesi olmaya davet etti.",
|
||||
"emails.invitation.footer": "Eğer ilgilenmiyorsanız devam etmeyin.",
|
||||
"emails.invitation.thanks": "Teşekkürler",
|
||||
"emails.invitation.signature": "{{project}} takımı",
|
||||
"emails.certificate.subject": "%s için sertifika hatası",
|
||||
"emails.certificate.hello": "Merhaba",
|
||||
"emails.certificate.body": "Alan adınız '{{domain}}' için sertifika oluşturulamadı. Deneme sayısı {{attempt}} ve hata sebebi: {{error}}",
|
||||
"emails.certificate.footer": "Geçmiş sertifikanız ilk denemeden sonra 30 gün daha geçerli kalacaktır. Bu konuyu araştırmanızı öneriyoruz, aksi taktirde alan adınız SSL sertifikasız kalacaktır.",
|
||||
"emails.certificate.thanks": "Teşekkürler",
|
||||
"emails.certificate.signature": "{{project}} takımı",
|
||||
"locale.country.unknown": "Bilinmeyen",
|
||||
"countries.af": "Afganistan",
|
||||
"countries.ao": "Angola",
|
||||
@@ -229,4 +235,4 @@
|
||||
"continents.na": "Kuzey Amerika",
|
||||
"continents.oc": "Okyanusya",
|
||||
"continents.sa": "Güney Amerika"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ return [
|
||||
[
|
||||
'key' => 'web',
|
||||
'name' => 'Console',
|
||||
'version' => '0.4.2',
|
||||
'version' => '0.5.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-console',
|
||||
'package' => '',
|
||||
'enabled' => true,
|
||||
@@ -196,7 +196,7 @@ return [
|
||||
'prism' => 'javascript',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/console-web'),
|
||||
'gitUrl' => 'https://github.com/appwrite/sdk-for-console.git',
|
||||
'gitBranch' => 'dev',
|
||||
'gitBranch' => 'main',
|
||||
'gitRepoName' => 'sdk-for-console',
|
||||
'gitUserName' => 'appwrite',
|
||||
],
|
||||
@@ -303,7 +303,7 @@ return [
|
||||
[
|
||||
'key' => 'ruby',
|
||||
'name' => 'Ruby',
|
||||
'version' => '10.1.0',
|
||||
'version' => '10.1.1',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-ruby',
|
||||
'package' => 'https://rubygems.org/gems/appwrite',
|
||||
'enabled' => true,
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -115,14 +115,6 @@ return [
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
// [
|
||||
// 'name' => '_APP_CONSOLE_WHITELIST_DOMAINS',
|
||||
// 'description' => 'This option allows you to limit creation of users to Appwrite console for users sharing the same email domains. This option is very useful for team working with company emails domain.\n\nTo enable this option, pass a list of allowed email domains separated by a comma.',
|
||||
// 'introduction' => '',
|
||||
// 'default' => '',
|
||||
// 'required' => false,
|
||||
// 'question' => '',
|
||||
// ],
|
||||
[
|
||||
'name' => '_APP_CONSOLE_WHITELIST_IPS',
|
||||
'description' => "This last option allows you to limit creation of users in Appwrite console for users sharing the same set of IP addresses. This option is very useful for team working with a VPN service or a company IP.\n\nTo enable/activate this option, pass a list of allowed IP addresses separated by a comma.",
|
||||
@@ -132,6 +124,15 @@ return [
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_CONSOLE_HOSTNAMES',
|
||||
'description' => 'This option allows you to add additional hostnames to your Appwrite console. This option is very useful for allowing access to the console project from additional domains. To enable it, pass a list of allowed hostnames separated by a comma.',
|
||||
'introduction' => '1.5.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_SYSTEM_EMAIL_NAME',
|
||||
'description' => 'This is the sender name value that will appear on email messages sent to developers from the Appwrite console. The default value is: \'Appwrite\'. You can use url encoded strings for spaces and special chars.',
|
||||
@@ -407,7 +408,7 @@ return [
|
||||
],
|
||||
[
|
||||
'name' => '_APP_SMTP_SECURE',
|
||||
'description' => 'SMTP secure connection protocol. Empty by default, change to \'tls\' if running on a secure connection.',
|
||||
'description' => 'SMTP secure connection protocol. Empty by default, change to \'tls\' or \'ssl\' if running on a secure connection.',
|
||||
'introduction' => '',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
@@ -947,6 +948,15 @@ return [
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_MAINTENANCE_DELAY',
|
||||
'description' => 'Delay value containing the number of seconds that the Appwrite maintenance process should wait before executing system cleanups and optimizations. The default value is 0 seconds.',
|
||||
'introduction' => '1.5.0',
|
||||
'default' => '0',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_MAINTENANCE_RETENTION_CACHE',
|
||||
'description' => 'The maximum duration (in seconds) upto which to retain cached files. The default value is 2592000 seconds (30 days).',
|
||||
|
||||
+1
-1
Submodule app/console updated: 3b6dd23ff5...0a007a3b1b
+140
-56
@@ -45,6 +45,7 @@ use Utopia\Validator\WhiteList;
|
||||
use Appwrite\Auth\Validator\PasswordHistory;
|
||||
use Appwrite\Auth\Validator\PasswordDictionary;
|
||||
use Appwrite\Auth\Validator\PersonalData;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Hooks\Hooks;
|
||||
|
||||
$oauthDefaultSuccess = '/auth/oauth2/success';
|
||||
@@ -109,7 +110,7 @@ App::post('/v1/account')
|
||||
Query::equal('providerEmail', [$email]),
|
||||
]);
|
||||
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
|
||||
}
|
||||
|
||||
if ($project->getAttribute('auths', [])['personalDataCheck'] ?? false) {
|
||||
@@ -119,7 +120,7 @@ App::post('/v1/account')
|
||||
}
|
||||
}
|
||||
|
||||
$hooks->trigger('passwordValidator', [$project, $password, $user]);
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
|
||||
|
||||
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||
$password = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
||||
@@ -196,7 +197,8 @@ App::post('/v1/account/sessions/email')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $email, string $password, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents) {
|
||||
->inject('hooks')
|
||||
->action(function (string $email, string $password, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Hooks $hooks) {
|
||||
|
||||
$email = \strtolower($email);
|
||||
$protocol = $request->getProtocol();
|
||||
@@ -215,6 +217,8 @@ App::post('/v1/account/sessions/email')
|
||||
|
||||
$user->setAttributes($profile->getArrayCopy());
|
||||
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, false]);
|
||||
|
||||
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
@@ -249,7 +253,7 @@ App::post('/v1/account/sessions/email')
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
}
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$session = $dbForProject->createDocument('sessions', $session->setAttribute('$permissions', [
|
||||
Permission::read(Role::user($user->getId())),
|
||||
@@ -549,11 +553,19 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
if (!$user->isEmpty()) {
|
||||
$userId = $user->getId();
|
||||
|
||||
$identitiesWithMatchingEmail = $dbForProject->find('identities', [
|
||||
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
|
||||
Query::equal('providerEmail', [$email]),
|
||||
Query::notEqual('userId', $userId),
|
||||
]);
|
||||
if (!empty($identitiesWithMatchingEmail)) {
|
||||
if (!empty($identityWithMatchingEmail)) {
|
||||
throw new Exception(Exception::USER_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$userWithMatchingEmail = $dbForProject->find('users', [
|
||||
Query::equal('email', [$email]),
|
||||
Query::notEqual('$id', $userId),
|
||||
]);
|
||||
if (!empty($userWithMatchingEmail)) {
|
||||
throw new Exception(Exception::USER_ALREADY_EXISTS);
|
||||
}
|
||||
}
|
||||
@@ -566,7 +578,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
$currentDocument = $dbForProject->getDocument('sessions', $current);
|
||||
if (!$currentDocument->isEmpty()) {
|
||||
$dbForProject->deleteDocument('sessions', $currentDocument->getId());
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,7 +637,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
Query::equal('providerEmail', [$email]),
|
||||
]);
|
||||
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -683,7 +695,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
Query::notEqual('userId', $user->getId()),
|
||||
]);
|
||||
if (!empty($identitiesWithMatchingEmail)) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
|
||||
}
|
||||
|
||||
$dbForProject->createDocument('identities', new Document([
|
||||
@@ -754,7 +766,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
]));
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$session->setAttribute('expire', $expire);
|
||||
|
||||
@@ -840,7 +852,7 @@ App::get('/v1/account/identities')
|
||||
});
|
||||
|
||||
App::delete('/v1/account/identities/:identityId')
|
||||
->desc('Delete Identity')
|
||||
->desc('Delete identity')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'account')
|
||||
->label('event', 'users.[userId].identities.[identityId].delete')
|
||||
@@ -856,7 +868,8 @@ App::delete('/v1/account/identities/:identityId')
|
||||
->param('identityId', '', new UID(), 'Identity ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $identityId, Response $response, Database $dbForProject) {
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $identityId, Response $response, Database $dbForProject, Event $queueForEvents) {
|
||||
|
||||
$identity = $dbForProject->getDocument('identities', $identityId);
|
||||
|
||||
@@ -866,12 +879,17 @@ App::delete('/v1/account/identities/:identityId')
|
||||
|
||||
$dbForProject->deleteDocument('identities', $identityId);
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $identity->getAttribute('userId'))
|
||||
->setParam('identityId', $identity->getId())
|
||||
->setPayload($response->output($identity, Response::MODEL_IDENTITY));
|
||||
|
||||
return $response->noContent();
|
||||
});
|
||||
|
||||
App::post('/v1/account/sessions/magic-url')
|
||||
->desc('Create magic URL session')
|
||||
->groups(['api', 'account'])
|
||||
->groups(['api', 'account', 'auth'])
|
||||
->label('scope', 'public')
|
||||
->label('auth.type', 'magic-url')
|
||||
->label('audits.event', 'session.create')
|
||||
@@ -884,8 +902,8 @@ App::post('/v1/account/sessions/magic-url')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_TOKEN)
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', 'url:{url},email:{param-email}')
|
||||
->label('abuse-limit', 60)
|
||||
->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}'])
|
||||
->param('userId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
|
||||
->param('email', '', new Email(), 'User email.')
|
||||
->param('url', '', fn($clients) => new Host($clients), 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients'])
|
||||
@@ -926,7 +944,7 @@ App::post('/v1/account/sessions/magic-url')
|
||||
Query::equal('providerEmail', [$email]),
|
||||
]);
|
||||
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
|
||||
}
|
||||
|
||||
$userId = $userId === 'unique()' ? ID::unique() : $userId;
|
||||
@@ -982,7 +1000,7 @@ App::post('/v1/account/sessions/magic-url')
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
]));
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
if (empty($url)) {
|
||||
$url = $request->getProtocol() . '://' . $request->getHostname() . '/auth/magic-url';
|
||||
@@ -998,7 +1016,7 @@ App::post('/v1/account/sessions/magic-url')
|
||||
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
|
||||
$message
|
||||
->setParam('{{body}}', $body)
|
||||
->setParam('{{body}}', $body, escapeHtml: false)
|
||||
->setParam('{{hello}}', $locale->getText("emails.magicSession.hello"))
|
||||
->setParam('{{footer}}', $locale->getText("emails.magicSession.footer"))
|
||||
->setParam('{{thanks}}', $locale->getText("emails.magicSession.thanks"))
|
||||
@@ -1053,11 +1071,11 @@ App::post('/v1/account/sessions/magic-url')
|
||||
|
||||
$emailVariables = [
|
||||
'direction' => $locale->getText('settings.direction'),
|
||||
/* {{user}} ,{{team}}, {{project}} and {{redirect}} are required in the templates */
|
||||
/* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */
|
||||
'user' => '',
|
||||
'team' => '',
|
||||
'project' => $project->getAttribute('name'),
|
||||
'redirect' => $url
|
||||
'redirect' => $url,
|
||||
'project' => $project->getAttribute('name')
|
||||
];
|
||||
|
||||
$queueForMails
|
||||
@@ -1159,7 +1177,7 @@ App::put('/v1/account/sessions/magic-url')
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
]));
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$tokens = $user->getAttribute('tokens', []);
|
||||
|
||||
@@ -1168,7 +1186,7 @@ App::put('/v1/account/sessions/magic-url')
|
||||
* the recovery token but actually we don't need it anymore.
|
||||
*/
|
||||
$dbForProject->deleteDocument('tokens', $token);
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$user->setAttribute('emailVerification', true);
|
||||
|
||||
@@ -1205,7 +1223,7 @@ App::put('/v1/account/sessions/magic-url')
|
||||
|
||||
App::post('/v1/account/sessions/phone')
|
||||
->desc('Create phone session')
|
||||
->groups(['api', 'account'])
|
||||
->groups(['api', 'account', 'auth'])
|
||||
->label('scope', 'public')
|
||||
->label('auth.type', 'phone')
|
||||
->label('audits.event', 'session.create')
|
||||
@@ -1219,7 +1237,7 @@ App::post('/v1/account/sessions/phone')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_TOKEN)
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', 'url:{url},phone:{param-phone}')
|
||||
->label('abuse-key', ['url:{url},phone:{param-phone}', 'url:{url},ip:{ip}'])
|
||||
->param('userId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
|
||||
->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
|
||||
->inject('request')
|
||||
@@ -1306,7 +1324,7 @@ App::post('/v1/account/sessions/phone')
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
]));
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
|
||||
|
||||
@@ -1321,6 +1339,7 @@ App::post('/v1/account/sessions/phone')
|
||||
$queueForMessaging
|
||||
->setRecipient($phone)
|
||||
->setMessage($message)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
|
||||
$queueForEvents->setPayload(
|
||||
@@ -1410,14 +1429,14 @@ App::put('/v1/account/sessions/phone')
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
]));
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
/**
|
||||
* We act like we're updating and validating
|
||||
* the recovery token but actually we don't need it anymore.
|
||||
*/
|
||||
$dbForProject->deleteDocument('tokens', $token);
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$user->setAttribute('phoneVerification', true);
|
||||
|
||||
@@ -1562,7 +1581,7 @@ App::post('/v1/account/sessions/anonymous')
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
]));
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
@@ -1879,7 +1898,8 @@ App::patch('/v1/account/password')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $password, string $oldPassword, ?\DateTime $requestTimestamp, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
->inject('hooks')
|
||||
->action(function (string $password, string $oldPassword, ?\DateTime $requestTimestamp, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
|
||||
|
||||
// Check old password only if its an existing user.
|
||||
if (!empty($user->getAttribute('passwordUpdate')) && !Auth::passwordVerify($oldPassword, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))) { // Double check user password
|
||||
@@ -1906,6 +1926,8 @@ App::patch('/v1/account/password')
|
||||
}
|
||||
}
|
||||
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
|
||||
|
||||
$user
|
||||
->setAttribute('password', $newPassword)
|
||||
->setAttribute('passwordHistory', $history)
|
||||
@@ -1943,7 +1965,9 @@ App::patch('/v1/account/email')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
|
||||
->inject('project')
|
||||
->inject('hooks')
|
||||
->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks) {
|
||||
// passwordUpdate will be empty if the user has never set a password
|
||||
$passwordUpdate = $user->getAttribute('passwordUpdate');
|
||||
|
||||
@@ -1954,6 +1978,8 @@ App::patch('/v1/account/email')
|
||||
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
|
||||
}
|
||||
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, false]);
|
||||
|
||||
$email = \strtolower($email);
|
||||
|
||||
// Makes sure this email is not already used in another identity
|
||||
@@ -1962,7 +1988,7 @@ App::patch('/v1/account/email')
|
||||
Query::notEqual('userId', $user->getId()),
|
||||
]);
|
||||
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
|
||||
}
|
||||
|
||||
$user
|
||||
@@ -1981,7 +2007,7 @@ App::patch('/v1/account/email')
|
||||
try {
|
||||
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
|
||||
} catch (Duplicate) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
|
||||
}
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
@@ -2012,7 +2038,9 @@ App::patch('/v1/account/phone')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $phone, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
|
||||
->inject('project')
|
||||
->inject('hooks')
|
||||
->action(function (string $phone, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks) {
|
||||
// passwordUpdate will be empty if the user has never set a password
|
||||
$passwordUpdate = $user->getAttribute('passwordUpdate');
|
||||
|
||||
@@ -2023,6 +2051,8 @@ App::patch('/v1/account/phone')
|
||||
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
|
||||
}
|
||||
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, false]);
|
||||
|
||||
$user
|
||||
->setAttribute('phone', $phone)
|
||||
->setAttribute('phoneVerification', false) // After this user needs to confirm phone number again
|
||||
@@ -2184,7 +2214,7 @@ App::delete('/v1/account/sessions/:sessionId')
|
||||
;
|
||||
}
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
@@ -2264,7 +2294,7 @@ App::patch('/v1/account/sessions/:sessionId')
|
||||
|
||||
$dbForProject->updateDocument('sessions', $sessionId, $session);
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
|
||||
@@ -2334,7 +2364,7 @@ App::delete('/v1/account/sessions')
|
||||
}
|
||||
}
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
@@ -2359,7 +2389,7 @@ App::post('/v1/account/recovery')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_TOKEN)
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', ['url:{url},email:{param-email}', 'ip:{ip}'])
|
||||
->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}'])
|
||||
->param('email', '', new Email(), 'User email.')
|
||||
->param('url', '', fn ($clients) => new Host($clients), 'URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients'])
|
||||
->inject('request')
|
||||
@@ -2419,7 +2449,7 @@ App::post('/v1/account/recovery')
|
||||
Permission::delete(Role::user($profile->getId())),
|
||||
]));
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $profile->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $profile->getId());
|
||||
|
||||
$url = Template::parseURL($url);
|
||||
$url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $profile->getId(), 'secret' => $secret, 'expire' => $expire]);
|
||||
@@ -2432,7 +2462,7 @@ App::post('/v1/account/recovery')
|
||||
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
|
||||
$message
|
||||
->setParam('{{body}}', $body)
|
||||
->setParam('{{body}}', $body, escapeHtml: false)
|
||||
->setParam('{{hello}}', $locale->getText("emails.recovery.hello"))
|
||||
->setParam('{{footer}}', $locale->getText("emails.recovery.footer"))
|
||||
->setParam('{{thanks}}', $locale->getText("emails.recovery.thanks"))
|
||||
@@ -2487,16 +2517,16 @@ App::post('/v1/account/recovery')
|
||||
|
||||
$emailVariables = [
|
||||
'direction' => $locale->getText('settings.direction'),
|
||||
/* {{user}} ,{{team}}, {{project}} and {{redirect}} are required in the templates */
|
||||
/* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */
|
||||
'user' => $profile->getAttribute('name'),
|
||||
'team' => '',
|
||||
'project' => $projectName,
|
||||
'redirect' => $url
|
||||
'redirect' => $url,
|
||||
'project' => $projectName
|
||||
];
|
||||
|
||||
$queueForMails
|
||||
->setRecipient($profile->getAttribute('email', ''))
|
||||
->setName($profile->getAttribute('name'))
|
||||
->setName($profile->getAttribute('name', ''))
|
||||
->setBody($body)
|
||||
->setVariables($emailVariables)
|
||||
->setSubject($subject)
|
||||
@@ -2546,7 +2576,8 @@ App::put('/v1/account/recovery')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $secret, string $password, string $passwordAgain, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents) {
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $secret, string $password, string $passwordAgain, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents, Hooks $hooks) {
|
||||
if ($password !== $passwordAgain) {
|
||||
throw new Exception(Exception::USER_PASSWORD_MISMATCH);
|
||||
}
|
||||
@@ -2580,6 +2611,8 @@ App::put('/v1/account/recovery')
|
||||
$history = array_slice($history, (count($history) - $historyLimit), $historyLimit);
|
||||
}
|
||||
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
|
||||
|
||||
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile
|
||||
->setAttribute('password', $newPassword)
|
||||
->setAttribute('passwordHistory', $history)
|
||||
@@ -2597,7 +2630,7 @@ App::put('/v1/account/recovery')
|
||||
* the recovery token but actually we don't need it anymore.
|
||||
*/
|
||||
$dbForProject->deleteDocument('tokens', $recovery);
|
||||
$dbForProject->deleteCachedDocument('users', $profile->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $profile->getId());
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $profile->getId())
|
||||
@@ -2668,7 +2701,7 @@ App::post('/v1/account/verification')
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
]));
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$url = Template::parseURL($url);
|
||||
$url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $user->getId(), 'secret' => $verificationSecret, 'expire' => $expire]);
|
||||
@@ -2681,7 +2714,7 @@ App::post('/v1/account/verification')
|
||||
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
|
||||
$message
|
||||
->setParam('{{body}}', $body)
|
||||
->setParam('{{body}}', $body, escapeHtml: false)
|
||||
->setParam('{{hello}}', $locale->getText("emails.verification.hello"))
|
||||
->setParam('{{footer}}', $locale->getText("emails.verification.footer"))
|
||||
->setParam('{{thanks}}', $locale->getText("emails.verification.thanks"))
|
||||
@@ -2736,11 +2769,11 @@ App::post('/v1/account/verification')
|
||||
|
||||
$emailVariables = [
|
||||
'direction' => $locale->getText('settings.direction'),
|
||||
/* {{user}} ,{{team}}, {{project}} and {{redirect}} are required in the templates */
|
||||
/* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */
|
||||
'user' => $user->getAttribute('name'),
|
||||
'team' => '',
|
||||
'project' => $projectName,
|
||||
'redirect' => $url
|
||||
'redirect' => $url,
|
||||
'project' => $projectName
|
||||
];
|
||||
|
||||
$queueForMails
|
||||
@@ -2817,7 +2850,7 @@ App::put('/v1/account/verification')
|
||||
* the verification token but actually we don't need it anymore.
|
||||
*/
|
||||
$dbForProject->deleteDocument('tokens', $verification);
|
||||
$dbForProject->deleteCachedDocument('users', $profile->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $profile->getId());
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $userId)
|
||||
@@ -2829,8 +2862,9 @@ App::put('/v1/account/verification')
|
||||
|
||||
App::post('/v1/account/verification/phone')
|
||||
->desc('Create phone verification')
|
||||
->groups(['api', 'account'])
|
||||
->groups(['api', 'account', 'auth'])
|
||||
->label('scope', 'account')
|
||||
->label('auth.type', 'phone')
|
||||
->label('event', 'users.[userId].verification.[tokenId].create')
|
||||
->label('audits.event', 'verification.create')
|
||||
->label('audits.resource', 'user/{response.userId}')
|
||||
@@ -2842,7 +2876,7 @@ App::post('/v1/account/verification/phone')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_TOKEN)
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', 'userId:{userId}')
|
||||
->label('abuse-key', ['url:{url},userId:{userId}', 'url:{url},ip:{ip}'])
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
@@ -2891,7 +2925,7 @@ App::post('/v1/account/verification/phone')
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
]));
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
|
||||
|
||||
@@ -2906,6 +2940,7 @@ App::post('/v1/account/verification/phone')
|
||||
$queueForMessaging
|
||||
->setRecipient($user->getAttribute('phone'))
|
||||
->setMessage($message)
|
||||
->setProject($project)
|
||||
->trigger()
|
||||
;
|
||||
|
||||
@@ -2974,7 +3009,7 @@ App::put('/v1/account/verification/phone')
|
||||
* We act like we're updating and validating the verification token but actually we don't need it anymore.
|
||||
*/
|
||||
$dbForProject->deleteDocument('tokens', $verification);
|
||||
$dbForProject->deleteCachedDocument('users', $profile->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $profile->getId());
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
@@ -2983,3 +3018,52 @@ App::put('/v1/account/verification/phone')
|
||||
|
||||
$response->dynamic($verificationDocument, Response::MODEL_TOKEN);
|
||||
});
|
||||
|
||||
App::delete('/v1/account')
|
||||
->desc('Delete account')
|
||||
->groups(['api', 'account'])
|
||||
->label('event', 'users.[userId].delete')
|
||||
->label('scope', 'account')
|
||||
->label('audits.event', 'user.delete')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.delete')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'delete')
|
||||
->label('sdk.description', '/docs/references/account/delete.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->inject('user')
|
||||
->inject('project')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForDeletes')
|
||||
->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes) {
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($project->getId() === 'console') {
|
||||
// get all memberships
|
||||
$memberships = $user->getAttribute('memberships', []);
|
||||
foreach ($memberships as $membership) {
|
||||
// prevent deletion if at least one active membership
|
||||
if ($membership->getAttribute('confirm', false)) {
|
||||
throw new Exception(Exception::USER_DELETION_PROHIBITED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$dbForProject->deleteDocument('users', $user->getId());
|
||||
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_DOCUMENT)
|
||||
->setDocument($user);
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
->setPayload($response->output($user, Response::MODEL_USER));
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
@@ -116,7 +116,7 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro
|
||||
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('sessions', $gitHubSession->getId(), $gitHubSession));
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
} catch (Throwable $err) {
|
||||
$index = 0;
|
||||
do {
|
||||
|
||||
@@ -153,13 +153,13 @@ function createAttribute(string $databaseId, string $collectionId, Document $att
|
||||
} catch (LimitException) {
|
||||
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, 'Attribute limit exceeded');
|
||||
} catch (\Exception $e) {
|
||||
$dbForProject->deleteCachedDocument('database_' . $db->getInternalId(), $collectionId);
|
||||
$dbForProject->deleteCachedCollection('database_' . $db->getInternalId() . '_collection_' . $collection->getInternalId());
|
||||
$dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId);
|
||||
$dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $collection->getInternalId());
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$dbForProject->deleteCachedDocument('database_' . $db->getInternalId(), $collectionId);
|
||||
$dbForProject->deleteCachedCollection('database_' . $db->getInternalId() . '_collection_' . $collection->getInternalId());
|
||||
$dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId);
|
||||
$dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $collection->getInternalId());
|
||||
|
||||
if ($type === Database::VAR_RELATIONSHIP && $options['twoWay']) {
|
||||
$twoWayKey = $options['twoWayKey'];
|
||||
@@ -197,13 +197,13 @@ function createAttribute(string $databaseId, string $collectionId, Document $att
|
||||
$dbForProject->deleteDocument('attributes', $attribute->getId());
|
||||
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, 'Attribute limit exceeded');
|
||||
} catch (\Exception $e) {
|
||||
$dbForProject->deleteCachedDocument('database_' . $db->getInternalId(), $relatedCollection->getId());
|
||||
$dbForProject->deleteCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedCollection->getInternalId());
|
||||
$dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $relatedCollection->getId());
|
||||
$dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedCollection->getInternalId());
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$dbForProject->deleteCachedDocument('database_' . $db->getInternalId(), $relatedCollection->getId());
|
||||
$dbForProject->deleteCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedCollection->getInternalId());
|
||||
$dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $relatedCollection->getId());
|
||||
$dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedCollection->getInternalId());
|
||||
}
|
||||
|
||||
$queueForDatabase
|
||||
@@ -358,7 +358,7 @@ function updateAttribute(
|
||||
$relatedOptions = \array_merge($relatedAttribute->getAttribute('options'), $options);
|
||||
$relatedAttribute->setAttribute('options', $relatedOptions);
|
||||
$dbForProject->updateDocument('attributes', $db->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $primaryDocumentOptions['twoWayKey'], $relatedAttribute);
|
||||
$dbForProject->deleteCachedDocument('database_' . $db->getInternalId(), $relatedCollection->getId());
|
||||
$dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $relatedCollection->getId());
|
||||
}
|
||||
} else {
|
||||
$dbForProject->updateAttribute(
|
||||
@@ -371,7 +371,7 @@ function updateAttribute(
|
||||
}
|
||||
|
||||
$attribute = $dbForProject->updateDocument('attributes', $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key, $attribute);
|
||||
$dbForProject->deleteCachedDocument('database_' . $db->getInternalId(), $collection->getId());
|
||||
$dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collection->getId());
|
||||
|
||||
$queueForEvents
|
||||
->setContext('collection', $collection)
|
||||
@@ -703,8 +703,8 @@ App::delete('/v1/databases/:databaseId')
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove collection from DB');
|
||||
}
|
||||
|
||||
$dbForProject->deleteCachedDocument('databases', $database->getId());
|
||||
$dbForProject->deleteCachedCollection('databases_' . $database->getInternalId());
|
||||
$dbForProject->purgeCachedDocument('databases', $database->getId());
|
||||
$dbForProject->purgeCachedCollection('databases_' . $database->getInternalId());
|
||||
|
||||
$queueForDatabase
|
||||
->setType(DATABASE_TYPE_DELETE_DATABASE)
|
||||
@@ -1076,7 +1076,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId')
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove collection from DB');
|
||||
}
|
||||
|
||||
$dbForProject->deleteCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId());
|
||||
$dbForProject->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId());
|
||||
|
||||
$queueForDatabase
|
||||
->setType(DATABASE_TYPE_DELETE_COLLECTION)
|
||||
@@ -2248,8 +2248,8 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key
|
||||
$attribute = $dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'deleting'));
|
||||
}
|
||||
|
||||
$dbForProject->deleteCachedDocument('database_' . $db->getInternalId(), $collectionId);
|
||||
$dbForProject->deleteCachedCollection('database_' . $db->getInternalId() . '_collection_' . $collection->getInternalId());
|
||||
$dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId);
|
||||
$dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $collection->getInternalId());
|
||||
|
||||
if ($attribute->getAttribute('type') === Database::VAR_RELATIONSHIP) {
|
||||
$options = $attribute->getAttribute('options');
|
||||
@@ -2270,8 +2270,8 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key
|
||||
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'deleting'));
|
||||
}
|
||||
|
||||
$dbForProject->deleteCachedDocument('database_' . $db->getInternalId(), $options['relatedCollection']);
|
||||
$dbForProject->deleteCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedCollection->getInternalId());
|
||||
$dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $options['relatedCollection']);
|
||||
$dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedCollection->getInternalId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2453,7 +2453,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
|
||||
throw new Exception(Exception::INDEX_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$dbForProject->deleteCachedDocument('database_' . $db->getInternalId(), $collectionId);
|
||||
$dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId);
|
||||
|
||||
$queueForDatabase
|
||||
->setType(DATABASE_TYPE_CREATE_INDEX)
|
||||
@@ -2620,7 +2620,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key')
|
||||
$index = $dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'deleting'));
|
||||
}
|
||||
|
||||
$dbForProject->deleteCachedDocument('database_' . $db->getInternalId(), $collectionId);
|
||||
$dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId);
|
||||
|
||||
$queueForDatabase
|
||||
->setType(DATABASE_TYPE_DELETE_INDEX)
|
||||
@@ -2997,6 +2997,33 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
|
||||
$processDocument($collection, $document);
|
||||
}
|
||||
|
||||
$select = \array_reduce($queries, function ($result, $query) {
|
||||
return $result || ($query->getMethod() === Query::TYPE_SELECT);
|
||||
}, false);
|
||||
|
||||
// Check if the SELECT query includes $databaseId and $collectionId
|
||||
$hasDatabaseId = false;
|
||||
$hasCollectionId = false;
|
||||
if ($select) {
|
||||
$hasDatabaseId = \array_reduce($queries, function ($result, $query) {
|
||||
return $result || ($query->getMethod() === Query::TYPE_SELECT && \in_array('$databaseId', $query->getValues()));
|
||||
}, false);
|
||||
$hasCollectionId = \array_reduce($queries, function ($result, $query) {
|
||||
return $result || ($query->getMethod() === Query::TYPE_SELECT && \in_array('$collectionId', $query->getValues()));
|
||||
}, false);
|
||||
}
|
||||
|
||||
if ($select) {
|
||||
foreach ($documents as $document) {
|
||||
if (!$hasDatabaseId) {
|
||||
$document->removeAttribute('$databaseId');
|
||||
}
|
||||
if (!$hasCollectionId) {
|
||||
$document->removeAttribute('$collectionId');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'total' => $total,
|
||||
'documents' => $documents,
|
||||
|
||||
@@ -456,7 +456,7 @@ App::get('/v1/functions/:functionId/usage')
|
||||
->label('sdk.method', 'getFunctionUsage')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USAGE_FUNCTIONS)
|
||||
->label('sdk.response.model', Response::MODEL_USAGE_FUNCTION)
|
||||
->param('functionId', '', new UID(), 'Function ID.')
|
||||
->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true)
|
||||
->inject('response')
|
||||
|
||||
+146
-10
@@ -7,6 +7,7 @@ use Appwrite\Utopia\Response;
|
||||
use Utopia\App;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Domains\Validator\PublicDomain;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Queue\Client;
|
||||
use Utopia\Queue\Connection;
|
||||
@@ -14,8 +15,11 @@ use Utopia\Registry\Registry;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\Storage\Device\Local;
|
||||
use Utopia\Storage\Storage;
|
||||
use Utopia\Validator\Domain;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Multiple;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
App::get('/v1/health')
|
||||
->desc('Get HTTP')
|
||||
@@ -355,7 +359,7 @@ App::get('/v1/health/queue/webhooks')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -382,12 +386,62 @@ App::get('/v1/health/queue/logs')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
}, ['response']);
|
||||
|
||||
App::get('/v1/health/certificate')
|
||||
->desc('Get the SSL certificate for a domain')
|
||||
->groups(['api', 'health'])
|
||||
->label('scope', 'health.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'health')
|
||||
->label('sdk.method', 'getCertificate')
|
||||
->label('sdk.description', '/docs/references/health/get-certificate.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_HEALTH_CERTIFICATE)
|
||||
->param('domain', null, new Multiple([new Domain(), new PublicDomain()]), Multiple::TYPE_STRING, 'Domain name')
|
||||
->inject('response')
|
||||
->action(function (string $domain, Response $response) {
|
||||
if (filter_var($domain, FILTER_VALIDATE_URL)) {
|
||||
$domain = parse_url($domain, PHP_URL_HOST);
|
||||
}
|
||||
|
||||
$sslContext = stream_context_create([
|
||||
"ssl" => [
|
||||
"capture_peer_cert" => true
|
||||
]
|
||||
]);
|
||||
$sslSocket = stream_socket_client("ssl://" . $domain . ":443", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $sslContext);
|
||||
if (!$sslSocket) {
|
||||
throw new Exception(Exception::HEALTH_INVALID_HOST);
|
||||
}
|
||||
|
||||
$streamContextParams = stream_context_get_params($sslSocket);
|
||||
$peerCertificate = $streamContextParams['options']['ssl']['peer_certificate'];
|
||||
$certificatePayload = openssl_x509_parse($peerCertificate);
|
||||
|
||||
|
||||
$sslExpiration = $certificatePayload['validTo_time_t'];
|
||||
$status = $sslExpiration < time() ? 'fail' : 'pass';
|
||||
|
||||
if ($status === 'fail') {
|
||||
throw new Exception(Exception::HEALTH_CERTIFICATE_EXPIRED);
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'name' => $certificatePayload['name'],
|
||||
'subjectSN' => $certificatePayload['subject']['CN'],
|
||||
'issuerOrganisation' => $certificatePayload['issuer']['O'],
|
||||
'validFrom' => $certificatePayload['validFrom_time_t'],
|
||||
'validTo' => $certificatePayload['validTo_time_t'],
|
||||
'signatureTypeSN' => $certificatePayload['signatureTypeSN'],
|
||||
]), Response::MODEL_HEALTH_CERTIFICATE);
|
||||
}, ['response']);
|
||||
|
||||
App::get('/v1/health/queue/certificates')
|
||||
->desc('Get certificates queue')
|
||||
->groups(['api', 'health'])
|
||||
@@ -409,7 +463,7 @@ App::get('/v1/health/queue/certificates')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -436,7 +490,7 @@ App::get('/v1/health/queue/builds')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -464,7 +518,7 @@ App::get('/v1/health/queue/databases')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -491,7 +545,7 @@ App::get('/v1/health/queue/deletes')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -518,7 +572,7 @@ App::get('/v1/health/queue/mails')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -545,7 +599,7 @@ App::get('/v1/health/queue/messaging')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -572,7 +626,7 @@ App::get('/v1/health/queue/migrations')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -599,7 +653,7 @@ App::get('/v1/health/queue/functions')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -648,6 +702,47 @@ App::get('/v1/health/storage/local')
|
||||
$response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS);
|
||||
});
|
||||
|
||||
App::get('/v1/health/storage')
|
||||
->desc('Get storage')
|
||||
->groups(['api', 'health'])
|
||||
->label('scope', 'health.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'health')
|
||||
->label('sdk.method', 'getStorage')
|
||||
->label('sdk.description', '/docs/references/health/get-storage.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_HEALTH_STATUS)
|
||||
->inject('response')
|
||||
->inject('deviceFiles')
|
||||
->inject('deviceFunctions')
|
||||
->inject('deviceBuilds')
|
||||
->action(function (Response $response, Device $deviceFiles, Device $deviceFunctions, Device $deviceBuilds) {
|
||||
$devices = [$deviceFiles, $deviceFunctions, $deviceBuilds];
|
||||
$checkStart = \microtime(true);
|
||||
|
||||
foreach ($devices as $device) {
|
||||
if (!$device->write($device->getPath('health.txt'), 'test', 'text/plain')) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed writing test file to ' . $device->getRoot());
|
||||
}
|
||||
|
||||
if ($device->read($device->getPath('health.txt')) !== 'test') {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed reading test file from ' . $device->getRoot());
|
||||
}
|
||||
|
||||
if (!$device->delete($device->getPath('health.txt'))) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed deleting test file from ' . $device->getRoot());
|
||||
}
|
||||
}
|
||||
|
||||
$output = [
|
||||
'status' => 'pass',
|
||||
'ping' => \round((\microtime(true) - $checkStart) / 1000)
|
||||
];
|
||||
|
||||
$response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS);
|
||||
});
|
||||
|
||||
App::get('/v1/health/anti-virus')
|
||||
->desc('Get antivirus')
|
||||
->groups(['api', 'health'])
|
||||
@@ -687,6 +782,47 @@ App::get('/v1/health/anti-virus')
|
||||
$response->dynamic(new Document($output), Response::MODEL_HEALTH_ANTIVIRUS);
|
||||
});
|
||||
|
||||
App::get('/v1/health/queue/failed/:name')
|
||||
->desc('Get number of failed queue jobs')
|
||||
->groups(['api', 'health'])
|
||||
->label('scope', 'health.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'health')
|
||||
->label('sdk.method', 'getFailedJobs')
|
||||
->param('name', '', new WhiteList([
|
||||
Event::DATABASE_QUEUE_NAME,
|
||||
Event::DELETE_QUEUE_NAME,
|
||||
Event::AUDITS_QUEUE_NAME,
|
||||
Event::MAILS_QUEUE_NAME,
|
||||
Event::FUNCTIONS_QUEUE_NAME,
|
||||
Event::USAGE_QUEUE_NAME,
|
||||
Event::WEBHOOK_CLASS_NAME,
|
||||
Event::CERTIFICATES_QUEUE_NAME,
|
||||
Event::BUILDS_QUEUE_NAME,
|
||||
Event::MESSAGING_QUEUE_NAME,
|
||||
Event::MIGRATIONS_QUEUE_NAME,
|
||||
Event::HAMSTER_CLASS_NAME
|
||||
]), 'The name of the queue')
|
||||
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
|
||||
->label('sdk.description', '/docs/references/health/get-failed-queue-jobs.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE)
|
||||
->inject('response')
|
||||
->inject('queue')
|
||||
->action(function (string $name, int|string $threshold, Response $response, Connection $queue) {
|
||||
$threshold = \intval($threshold);
|
||||
|
||||
$client = new Client($name, $queue);
|
||||
$failed = $client->countFailedJobs();
|
||||
|
||||
if ($failed >= $threshold) {
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue failed jobs threshold hit. Current size is {$failed} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $failed ]), Response::MODEL_HEALTH_QUEUE);
|
||||
});
|
||||
|
||||
App::get('/v1/health/stats') // Currently only used internally
|
||||
->desc('Get system stats')
|
||||
->groups(['api', 'health'])
|
||||
|
||||
@@ -144,9 +144,30 @@ App::get('/v1/project/usage')
|
||||
];
|
||||
}, $dbForProject->find('buckets'));
|
||||
|
||||
// merge network inbound + outbound
|
||||
$projectBandwidth = [];
|
||||
foreach ($usage[METRIC_NETWORK_INBOUND] as $item) {
|
||||
$projectBandwidth[$item['date']] ??= 0;
|
||||
$projectBandwidth[$item['date']] += $item['value'];
|
||||
}
|
||||
|
||||
foreach ($usage[METRIC_NETWORK_OUTBOUND] as $item) {
|
||||
$projectBandwidth[$item['date']] ??= 0;
|
||||
$projectBandwidth[$item['date']] += $item['value'];
|
||||
}
|
||||
|
||||
|
||||
$network = [];
|
||||
foreach ($projectBandwidth as $date => $value) {
|
||||
$network[] = [
|
||||
'date' => $date,
|
||||
'value' => $value
|
||||
];
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'requests' => ($usage[METRIC_NETWORK_REQUESTS]),
|
||||
'network' => ($usage[METRIC_NETWORK_INBOUND] + $usage[METRIC_NETWORK_OUTBOUND]),
|
||||
'network' => $network,
|
||||
'users' => ($usage[METRIC_USERS]),
|
||||
'executions' => ($usage[METRIC_EXECUTIONS]),
|
||||
'executionsTotal' => $total[METRIC_EXECUTIONS],
|
||||
|
||||
@@ -9,6 +9,7 @@ use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Database\Validator\ProjectId;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Projects;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Utopia\Abuse\Adapters\TimeLimit;
|
||||
@@ -71,12 +72,12 @@ App::post('/v1/projects')
|
||||
->param('legalCity', '', new Text(256), 'Project legal City. Max length: 256 chars.', true)
|
||||
->param('legalAddress', '', new Text(256), 'Project legal Address. Max length: 256 chars.', true)
|
||||
->param('legalTaxId', '', new Text(256), 'Project legal Tax ID. Max length: 256 chars.', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->inject('cache')
|
||||
->inject('pools')
|
||||
->action(function (string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForConsole, Cache $cache, Group $pools) {
|
||||
|
||||
->action(function (string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Request $request, Response $response, Database $dbForConsole, Cache $cache, Group $pools) {
|
||||
|
||||
$team = $dbForConsole->getDocument('teams', $teamId);
|
||||
|
||||
@@ -85,8 +86,15 @@ App::post('/v1/projects')
|
||||
}
|
||||
|
||||
$auth = Config::getParam('auth', []);
|
||||
$auths = ['limit' => 0, 'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT, 'passwordHistory' => 0, 'passwordDictionary' => false, 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, 'personalDataCheck' => false];
|
||||
foreach ($auth as $index => $method) {
|
||||
$auths = [
|
||||
'limit' => 0,
|
||||
'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT,
|
||||
'passwordHistory' => 0,
|
||||
'passwordDictionary' => false,
|
||||
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG,
|
||||
'personalDataCheck' => false
|
||||
];
|
||||
foreach ($auth as $method) {
|
||||
$auths[$method['key'] ?? ''] = true;
|
||||
}
|
||||
|
||||
@@ -121,7 +129,7 @@ App::post('/v1/projects')
|
||||
}
|
||||
}
|
||||
|
||||
$databaseOverride = App::getEnv('_APP_DATABASE_OVERRIDE', null);
|
||||
$databaseOverride = App::getEnv('_APP_DATABASE_OVERRIDE');
|
||||
$index = array_search($databaseOverride, $databases);
|
||||
if ($index !== false) {
|
||||
$database = $databases[$index];
|
||||
@@ -133,6 +141,23 @@ App::post('/v1/projects')
|
||||
throw new Exception(Exception::PROJECT_RESERVED_PROJECT, "'console' is a reserved project.");
|
||||
}
|
||||
|
||||
// TODO: One in 20 projects use shared tables. Temporary until all projects are using shared tables.
|
||||
if (
|
||||
!\mt_rand(0, 19)
|
||||
&& App::getEnv('_APP_EDITION', 'self-hosted') !== 'self-hosted'
|
||||
) {
|
||||
$database = DATABASE_SHARED_TABLES;
|
||||
}
|
||||
|
||||
// TODO: Allow overriding in development mode. Temporary until all projects are using shared tables.
|
||||
if (
|
||||
App::isDevelopment()
|
||||
&& App::getEnv('_APP_EDITION', 'self-hosted') !== 'self-hosted'
|
||||
&& $request->getHeader('x-appwrite-share-tables', false)
|
||||
) {
|
||||
$database = DATABASE_SHARED_TABLES;
|
||||
}
|
||||
|
||||
try {
|
||||
$project = $dbForConsole->createDocument('projects', new Document([
|
||||
'$id' => $projectId,
|
||||
@@ -164,14 +189,26 @@ App::post('/v1/projects')
|
||||
'keys' => null,
|
||||
'auths' => $auths,
|
||||
'search' => implode(' ', [$projectId, $name]),
|
||||
'database' => $database
|
||||
'database' => $database,
|
||||
]));
|
||||
} catch (Duplicate $th) {
|
||||
} catch (Duplicate) {
|
||||
throw new Exception(Exception::PROJECT_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$dbForProject = new Database($pools->get($database)->pop()->getResource(), $cache);
|
||||
$dbForProject->setNamespace("_{$project->getInternalId()}");
|
||||
|
||||
if ($database === DATABASE_SHARED_TABLES) {
|
||||
$dbForProject
|
||||
->setShareTables(true)
|
||||
->setTenant($project->getInternalId())
|
||||
->setNamespace('');
|
||||
} else {
|
||||
$dbForProject
|
||||
->setShareTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getInternalId());
|
||||
}
|
||||
|
||||
$dbForProject->create();
|
||||
|
||||
$audit = new Audit($dbForProject);
|
||||
@@ -188,33 +225,19 @@ App::post('/v1/projects')
|
||||
continue;
|
||||
}
|
||||
|
||||
$attributes = [];
|
||||
$indexes = [];
|
||||
$attributes = \array_map(function (array $attribute) {
|
||||
return new Document($attribute);
|
||||
}, $collection['attributes']);
|
||||
|
||||
foreach ($collection['attributes'] as $attribute) {
|
||||
$attributes[] = new Document([
|
||||
'$id' => $attribute['$id'],
|
||||
'type' => $attribute['type'],
|
||||
'size' => $attribute['size'],
|
||||
'required' => $attribute['required'],
|
||||
'signed' => $attribute['signed'],
|
||||
'array' => $attribute['array'],
|
||||
'filters' => $attribute['filters'],
|
||||
'default' => $attribute['default'] ?? null,
|
||||
'format' => $attribute['format'] ?? ''
|
||||
]);
|
||||
}
|
||||
$indexes = \array_map(function (array $index) {
|
||||
return new Document($index);
|
||||
}, $collection['indexes']);
|
||||
|
||||
foreach ($collection['indexes'] as $index) {
|
||||
$indexes[] = new Document([
|
||||
'$id' => $index['$id'],
|
||||
'type' => $index['type'],
|
||||
'attributes' => $index['attributes'],
|
||||
'lengths' => $index['lengths'],
|
||||
'orders' => $index['orders'],
|
||||
]);
|
||||
try {
|
||||
$dbForProject->createCollection($key, $attributes, $indexes);
|
||||
} catch (Duplicate) {
|
||||
// Collection already exists
|
||||
}
|
||||
$dbForProject->createCollection($key, $attributes, $indexes);
|
||||
}
|
||||
|
||||
$response
|
||||
@@ -820,7 +843,7 @@ App::post('/v1/projects/:projectId/webhooks')
|
||||
|
||||
$webhook = $dbForConsole->createDocument('webhooks', $webhook);
|
||||
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
$dbForConsole->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -941,7 +964,7 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
|
||||
->setAttribute('httpPass', $httpPass);
|
||||
|
||||
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
$dbForConsole->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
|
||||
});
|
||||
@@ -980,7 +1003,7 @@ App::patch('/v1/projects/:projectId/webhooks/:webhookId/signature')
|
||||
$webhook->setAttribute('signatureKey', \bin2hex(\random_bytes(64)));
|
||||
|
||||
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
$dbForConsole->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
|
||||
});
|
||||
@@ -1017,7 +1040,7 @@ App::delete('/v1/projects/:projectId/webhooks/:webhookId')
|
||||
|
||||
$dbForConsole->deleteDocument('webhooks', $webhook->getId());
|
||||
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
$dbForConsole->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
@@ -1067,7 +1090,7 @@ App::post('/v1/projects/:projectId/keys')
|
||||
|
||||
$key = $dbForConsole->createDocument('keys', $key);
|
||||
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
$dbForConsole->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -1181,7 +1204,7 @@ App::put('/v1/projects/:projectId/keys/:keyId')
|
||||
|
||||
$dbForConsole->updateDocument('keys', $key->getId(), $key);
|
||||
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
$dbForConsole->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->dynamic($key, Response::MODEL_KEY);
|
||||
});
|
||||
@@ -1218,7 +1241,7 @@ App::delete('/v1/projects/:projectId/keys/:keyId')
|
||||
|
||||
$dbForConsole->deleteDocument('keys', $key->getId());
|
||||
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
$dbForConsole->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
@@ -1268,7 +1291,7 @@ App::post('/v1/projects/:projectId/platforms')
|
||||
|
||||
$platform = $dbForConsole->createDocument('platforms', $platform);
|
||||
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
$dbForConsole->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -1383,7 +1406,7 @@ App::put('/v1/projects/:projectId/platforms/:platformId')
|
||||
|
||||
$dbForConsole->updateDocument('platforms', $platform->getId(), $platform);
|
||||
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
$dbForConsole->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->dynamic($platform, Response::MODEL_PLATFORM);
|
||||
});
|
||||
@@ -1420,7 +1443,7 @@ App::delete('/v1/projects/:projectId/platforms/:platformId')
|
||||
|
||||
$dbForConsole->deleteDocument('platforms', $platformId);
|
||||
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
$dbForConsole->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
@@ -1446,7 +1469,7 @@ App::patch('/v1/projects/:projectId/smtp')
|
||||
->param('port', 587, new Integer(), 'SMTP server port', true)
|
||||
->param('username', '', new Text(0, 0), 'SMTP server username', true)
|
||||
->param('password', '', new Text(0, 0), 'SMTP server password', true)
|
||||
->param('secure', '', new WhiteList(['tls'], true), 'Does SMTP server use secure connection', true)
|
||||
->param('secure', '', new WhiteList(['tls', 'ssl'], true), 'Does SMTP server use secure connection', true)
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $projectId, bool $enabled, string $senderName, string $senderEmail, string $replyTo, string $host, int $port, string $username, string $password, string $secure, Response $response, Database $dbForConsole) {
|
||||
@@ -1590,7 +1613,7 @@ App::get('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
$message
|
||||
->setParam('{{hello}}', $localeObj->getText("emails.{$type}.hello"))
|
||||
->setParam('{{footer}}', $localeObj->getText("emails.{$type}.footer"))
|
||||
->setParam('{{body}}', $localeObj->getText('emails.' . $type . '.body'))
|
||||
->setParam('{{body}}', $localeObj->getText('emails.' . $type . '.body'), escapeHtml: false)
|
||||
->setParam('{{thanks}}', $localeObj->getText("emails.{$type}.thanks"))
|
||||
->setParam('{{signature}}', $localeObj->getText("emails.{$type}.signature"))
|
||||
->setParam('{{direction}}', $localeObj->getText('settings.direction'));
|
||||
|
||||
@@ -14,6 +14,7 @@ use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Domains\Domain;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Validator\Domain as ValidatorDomain;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
@@ -278,7 +279,8 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
|
||||
->inject('queueForEvents')
|
||||
->inject('project')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $ruleId, Response $response, Certificate $queueForCertificates, Event $queueForEvents, Document $project, Database $dbForConsole) {
|
||||
->inject('log')
|
||||
->action(function (string $ruleId, Response $response, Certificate $queueForCertificates, Event $queueForEvents, Document $project, Database $dbForConsole, Log $log) {
|
||||
$rule = $dbForConsole->getDocument('rules', $ruleId);
|
||||
|
||||
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getInternalId()) {
|
||||
@@ -298,7 +300,14 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
|
||||
$validator = new CNAME($target->get()); // Verify Domain with DNS records
|
||||
$domain = new Domain($rule->getAttribute('domain', ''));
|
||||
|
||||
$validationStart = \microtime(true);
|
||||
if (!$validator->isValid($domain->get())) {
|
||||
$log->addExtra('dnsTiming', \strval(\microtime(true) - $validationStart));
|
||||
$log->addTag('dnsDomain', $domain->get());
|
||||
|
||||
$error = $validator->getLogs();
|
||||
$log->addExtra('dnsResponse', \is_array($error) ? \json_encode($error) : \strval($error));
|
||||
|
||||
throw new Exception(Exception::RULE_VERIFICATION_FAILED);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Swoole\Request;
|
||||
use Utopia\Storage\Compression\Compression;
|
||||
|
||||
App::post('/v1/storage/buckets')
|
||||
->desc('Create bucket')
|
||||
@@ -66,7 +67,7 @@ App::post('/v1/storage/buckets')
|
||||
->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true)
|
||||
->param('maximumFileSize', (int) App::getEnv('_APP_STORAGE_LIMIT', 0), new Range(1, (int) App::getEnv('_APP_STORAGE_LIMIT', 0)), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true)
|
||||
->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true)
|
||||
->param('compression', COMPRESSION_TYPE_NONE, new WhiteList([COMPRESSION_TYPE_NONE, COMPRESSION_TYPE_GZIP, COMPRESSION_TYPE_ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . COMPRESSION_TYPE_NONE . ', [' . COMPRESSION_TYPE_GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . COMPRESSION_TYPE_ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
|
||||
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
|
||||
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
|
||||
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
|
||||
->inject('response')
|
||||
@@ -237,7 +238,7 @@ App::put('/v1/storage/buckets/:bucketId')
|
||||
->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true)
|
||||
->param('maximumFileSize', null, new Range(1, (int) App::getEnv('_APP_STORAGE_LIMIT', 0)), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human((int)App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true)
|
||||
->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true)
|
||||
->param('compression', COMPRESSION_TYPE_NONE, new WhiteList([COMPRESSION_TYPE_NONE, COMPRESSION_TYPE_GZIP, COMPRESSION_TYPE_ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . COMPRESSION_TYPE_NONE . ', [' . COMPRESSION_TYPE_GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . COMPRESSION_TYPE_ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
|
||||
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
|
||||
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
|
||||
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
|
||||
->inject('response')
|
||||
@@ -531,19 +532,24 @@ App::post('/v1/storage/buckets/:bucketId/files')
|
||||
$fileHash = $deviceFiles->getFileHash($path); // Get file hash before compression and encryption
|
||||
$data = '';
|
||||
// Compression
|
||||
$algorithm = $bucket->getAttribute('compression', COMPRESSION_TYPE_NONE);
|
||||
if ($fileSize <= APP_STORAGE_READ_BUFFER && $algorithm != COMPRESSION_TYPE_NONE) {
|
||||
$algorithm = $bucket->getAttribute('compression', Compression::NONE);
|
||||
if ($fileSize <= APP_STORAGE_READ_BUFFER && $algorithm != Compression::NONE) {
|
||||
$data = $deviceFiles->read($path);
|
||||
switch ($algorithm) {
|
||||
case COMPRESSION_TYPE_ZSTD:
|
||||
case Compression::ZSTD:
|
||||
$compressor = new Zstd();
|
||||
break;
|
||||
case COMPRESSION_TYPE_GZIP:
|
||||
case Compression::GZIP:
|
||||
default:
|
||||
$compressor = new GZIP();
|
||||
break;
|
||||
}
|
||||
$data = $compressor->compress($data);
|
||||
} else {
|
||||
// reset the algorithm to none as we do not compress the file
|
||||
// if file size exceedes the APP_STORAGE_READ_BUFFER
|
||||
// regardless the bucket compression algoorithm
|
||||
$algorithm = Compression::NONE;
|
||||
}
|
||||
|
||||
if ($bucket->getAttribute('encryption', true) && $fileSize <= APP_STORAGE_READ_BUFFER) {
|
||||
@@ -615,7 +621,17 @@ App::post('/v1/storage/buckets/:bucketId/files')
|
||||
->setAttribute('metadata', $metadata)
|
||||
->setAttribute('chunksUploaded', $chunksUploaded);
|
||||
|
||||
$file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);
|
||||
/**
|
||||
* Validate create permission and skip authorization in updateDocument
|
||||
* Without this, the file creation will fail when user doesn't have update permission
|
||||
* However as with chunk upload even if we are updating, we are essentially creating a file
|
||||
* adding it's new chunk so we validate create permission instead of update
|
||||
*/
|
||||
$validator = new Authorization(Database::PERMISSION_CREATE);
|
||||
if (!$validator->isValid($bucket->getCreate())) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
$file = Authorization::skip(fn() => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file));
|
||||
}
|
||||
} catch (AuthorizationException) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
@@ -652,7 +668,17 @@ App::post('/v1/storage/buckets/:bucketId/files')
|
||||
->setAttribute('chunksUploaded', $chunksUploaded)
|
||||
->setAttribute('metadata', $metadata);
|
||||
|
||||
$file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);
|
||||
/**
|
||||
* Validate create permission and skip authorization in updateDocument
|
||||
* Without this, the file creation will fail when user doesn't have update permission
|
||||
* However as with chunk upload even if we are updating, we are essentially creating a file
|
||||
* adding it's new chunk so we validate create permission instead of update
|
||||
*/
|
||||
$validator = new Authorization(Database::PERMISSION_CREATE);
|
||||
if (!$validator->isValid($bucket->getCreate())) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
$file = Authorization::skip(fn() => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file));
|
||||
}
|
||||
} catch (AuthorizationException) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
@@ -859,14 +885,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if ((\strpos($request->getAccept(), 'image/webp') === false) && ('webp' === $output)) { // Fallback webp to jpeg when no browser support
|
||||
$output = 'jpg';
|
||||
}
|
||||
|
||||
$inputs = Config::getParam('storage-inputs');
|
||||
$outputs = Config::getParam('storage-outputs');
|
||||
$fileLogos = Config::getParam('storage-logos');
|
||||
|
||||
if ($fileSecurity && !$valid) {
|
||||
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
|
||||
} else {
|
||||
@@ -877,9 +895,17 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ((\strpos($request->getAccept(), 'image/webp') === false) && ('webp' === $output)) { // Fallback webp to jpeg when no browser support
|
||||
$output = 'jpg';
|
||||
}
|
||||
|
||||
$inputs = Config::getParam('storage-inputs');
|
||||
$outputs = Config::getParam('storage-outputs');
|
||||
$fileLogos = Config::getParam('storage-logos');
|
||||
|
||||
$path = $file->getAttribute('path');
|
||||
$type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION));
|
||||
$algorithm = $file->getAttribute('algorithm', 'none');
|
||||
$algorithm = $file->getAttribute('algorithm', Compression::NONE);
|
||||
$cipher = $file->getAttribute('openSSLCipher');
|
||||
$mime = $file->getAttribute('mimeType');
|
||||
if (!\in_array($mime, $inputs) || $file->getAttribute('sizeActual') > (int) App::getEnv('_APP_STORAGE_PREVIEW_LIMIT', 20000000)) {
|
||||
@@ -890,7 +916,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
|
||||
$path = $fileLogos['default_image'];
|
||||
}
|
||||
|
||||
$algorithm = 'none';
|
||||
$algorithm = Compression::NONE;
|
||||
$cipher = null;
|
||||
$background = (empty($background)) ? 'eceff1' : $background;
|
||||
$type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION));
|
||||
@@ -902,12 +928,17 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
|
||||
}
|
||||
|
||||
if (empty($output)) {
|
||||
// when file extension is provided but it's not one of our
|
||||
// supported outputs we fallback to `jpg`
|
||||
if (!empty($type) && !array_key_exists($type, $outputs)) {
|
||||
$type = 'jpg';
|
||||
}
|
||||
|
||||
// when file extension is not provided and the mime type is not one of our supported outputs
|
||||
// we fallback to `jpg` output format
|
||||
$output = empty($type) ? (array_search($mime, $outputs) ?? 'jpg') : $type;
|
||||
}
|
||||
|
||||
|
||||
$source = $deviceFiles->read($path);
|
||||
|
||||
if (!empty($cipher)) { // Decrypt
|
||||
@@ -922,11 +953,11 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
|
||||
}
|
||||
|
||||
switch ($algorithm) {
|
||||
case 'zstd':
|
||||
case Compression::ZSTD:
|
||||
$compressor = new Zstd();
|
||||
$source = $compressor->decompress($source);
|
||||
break;
|
||||
case 'gzip':
|
||||
case Compression::GZIP:
|
||||
$compressor = new GZIP();
|
||||
$source = $compressor->decompress($source);
|
||||
break;
|
||||
@@ -1065,15 +1096,15 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
|
||||
);
|
||||
}
|
||||
|
||||
switch ($file->getAttribute('algorithm', 'none')) {
|
||||
case 'zstd':
|
||||
switch ($file->getAttribute('algorithm', Compression::NONE)) {
|
||||
case Compression::ZSTD:
|
||||
if (empty($source)) {
|
||||
$source = $deviceFiles->read($path);
|
||||
}
|
||||
$compressor = new Zstd();
|
||||
$source = $compressor->decompress($source);
|
||||
break;
|
||||
case 'gzip':
|
||||
case Compression::GZIP:
|
||||
if (empty($source)) {
|
||||
$source = $deviceFiles->read($path);
|
||||
}
|
||||
@@ -1214,15 +1245,15 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
|
||||
);
|
||||
}
|
||||
|
||||
switch ($file->getAttribute('algorithm', 'none')) {
|
||||
case 'zstd':
|
||||
switch ($file->getAttribute('algorithm', Compression::NONE)) {
|
||||
case Compression::ZSTD:
|
||||
if (empty($source)) {
|
||||
$source = $deviceFiles->read($path);
|
||||
}
|
||||
$compressor = new Zstd();
|
||||
$source = $compressor->decompress($source);
|
||||
break;
|
||||
case 'gzip':
|
||||
case Compression::GZIP:
|
||||
if (empty($source)) {
|
||||
$source = $deviceFiles->read($path);
|
||||
}
|
||||
@@ -1236,10 +1267,12 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
|
||||
$response->send(substr($source, $start, ($end - $start + 1)));
|
||||
}
|
||||
$response->send($source);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($rangeHeader)) {
|
||||
$response->send($deviceFiles->read($path, $start, ($end - $start + 1)));
|
||||
return;
|
||||
}
|
||||
|
||||
$size = $deviceFiles->getFileSize($path);
|
||||
|
||||
@@ -114,7 +114,7 @@ App::post('/v1/teams')
|
||||
]);
|
||||
|
||||
$membership = $dbForProject->createDocument('memberships', $membership);
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
}
|
||||
|
||||
$queueForEvents->setParam('teamId', $team->getId());
|
||||
@@ -533,10 +533,10 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
} catch (Duplicate $th) {
|
||||
throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS);
|
||||
}
|
||||
$team->setAttribute('total', $team->getAttribute('total', 0) + 1);
|
||||
$team = Authorization::skip(fn() => $dbForProject->updateDocument('teams', $team->getId(), $team));
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $invitee->getId());
|
||||
Authorization::skip(fn() => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $invitee->getId());
|
||||
} else {
|
||||
try {
|
||||
$membership = $dbForProject->createDocument('memberships', $membership);
|
||||
@@ -556,7 +556,7 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
|
||||
$message
|
||||
->setParam('{{body}}', $body)
|
||||
->setParam('{{body}}', $body, escapeHtml: false)
|
||||
->setParam('{{hello}}', $locale->getText("emails.invitation.hello"))
|
||||
->setParam('{{footer}}', $locale->getText("emails.invitation.footer"))
|
||||
->setParam('{{thanks}}', $locale->getText("emails.invitation.thanks"))
|
||||
@@ -612,11 +612,11 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
$emailVariables = [
|
||||
'owner' => $user->getAttribute('name'),
|
||||
'direction' => $locale->getText('settings.direction'),
|
||||
/* {{user}} ,{{team}}, {{project}} and {{redirect}} are required in the templates */
|
||||
/* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */
|
||||
'user' => $user->getAttribute('name'),
|
||||
'team' => $team->getAttribute('name'),
|
||||
'project' => $projectName,
|
||||
'redirect' => $url
|
||||
'redirect' => $url,
|
||||
'project' => $projectName
|
||||
];
|
||||
|
||||
$queueForMails
|
||||
@@ -641,6 +641,7 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
$queueForMessaging
|
||||
->setRecipient($phone)
|
||||
->setMessage($message)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
@@ -843,7 +844,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
|
||||
/**
|
||||
* Replace membership on profile
|
||||
*/
|
||||
$dbForProject->deleteCachedDocument('users', $profile->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $profile->getId());
|
||||
|
||||
$queueForEvents
|
||||
->setParam('teamId', $team->getId())
|
||||
@@ -958,15 +959,15 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
]));
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
Authorization::setRole(Role::user($userId)->toString());
|
||||
|
||||
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership);
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$team = Authorization::skip(fn() => $dbForProject->updateDocument('teams', $team->getId(), $team->setAttribute('total', $team->getAttribute('total', 0) + 1)));
|
||||
Authorization::skip(fn() => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('teamId', $team->getId())
|
||||
@@ -1043,11 +1044,10 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove membership from DB');
|
||||
}
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
if ($membership->getAttribute('confirm')) { // Count only confirmed members
|
||||
$team->setAttribute('total', \max($team->getAttribute('total', 0) - 1, 0));
|
||||
Authorization::skip(fn() => $dbForProject->updateDocument('teams', $team->getId(), $team));
|
||||
Authorization::skip(fn() => $dbForProject->decreaseDocumentAttribute('teams', $team->getId(), 'total', 1, 0));
|
||||
}
|
||||
|
||||
$queueForEvents
|
||||
|
||||
@@ -9,6 +9,7 @@ use Appwrite\Event\Event;
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Identities;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Users;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
@@ -39,10 +40,12 @@ use Utopia\Validator\Integer;
|
||||
use Appwrite\Auth\Validator\PasswordHistory;
|
||||
use Appwrite\Auth\Validator\PasswordDictionary;
|
||||
use Appwrite\Auth\Validator\PersonalData;
|
||||
use Appwrite\Hooks\Hooks;
|
||||
|
||||
/** TODO: Remove function when we move to using utopia/platform */
|
||||
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Event $queueForEvents): Document
|
||||
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks): Document
|
||||
{
|
||||
$plaintextPassword = $password;
|
||||
$hashOptionsObject = (\is_string($hashOptions)) ? \json_decode($hashOptions, true) : $hashOptions; // Cast to JSON array
|
||||
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||
|
||||
@@ -65,13 +68,13 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
|
||||
|
||||
if ($project->getAttribute('auths', [])['personalDataCheck'] ?? false) {
|
||||
$personalDataValidator = new PersonalData($userId, $email, $name, $phone);
|
||||
if (!$personalDataValidator->isValid($password)) {
|
||||
if (!$personalDataValidator->isValid($plaintextPassword)) {
|
||||
throw new Exception(Exception::USER_PASSWORD_PERSONAL_DATA);
|
||||
}
|
||||
}
|
||||
|
||||
$password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null;
|
||||
$user = $dbForProject->createDocument('users', new Document([
|
||||
$user = new Document([
|
||||
'$id' => $userId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
@@ -97,7 +100,13 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
|
||||
'tokens' => null,
|
||||
'memberships' => null,
|
||||
'search' => implode(' ', [$userId, $email, $phone, $name]),
|
||||
]));
|
||||
]);
|
||||
|
||||
if ($hash === 'plaintext') {
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $plaintextPassword, &$user, true]);
|
||||
}
|
||||
|
||||
$user = $dbForProject->createDocument('users', $user);
|
||||
} catch (Duplicate $th) {
|
||||
throw new Exception(Exception::USER_ALREADY_EXISTS);
|
||||
}
|
||||
@@ -130,9 +139,9 @@ App::post('/v1/users')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
$user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $project, $dbForProject, $queueForEvents);
|
||||
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
|
||||
$user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $project, $dbForProject, $queueForEvents, $hooks);
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($user, Response::MODEL_USER);
|
||||
@@ -160,8 +169,9 @@ App::post('/v1/users/bcrypt')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
$user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents);
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
|
||||
$user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -190,8 +200,9 @@ App::post('/v1/users/md5')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
$user = createUser('md5', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents);
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
|
||||
$user = createUser('md5', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -220,8 +231,9 @@ App::post('/v1/users/argon2')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
$user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents);
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
|
||||
$user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -251,14 +263,15 @@ App::post('/v1/users/sha')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
|
||||
$options = '{}';
|
||||
|
||||
if (!empty($passwordVersion)) {
|
||||
$options = '{"version":"' . $passwordVersion . '"}';
|
||||
}
|
||||
|
||||
$user = createUser('sha', $options, $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents);
|
||||
$user = createUser('sha', $options, $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -287,8 +300,9 @@ App::post('/v1/users/phpass')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
$user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents);
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
|
||||
$user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -322,7 +336,8 @@ App::post('/v1/users/scrypt')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->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, Event $queueForEvents) {
|
||||
->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, Event $queueForEvents, Hooks $hooks) {
|
||||
$options = [
|
||||
'salt' => $passwordSalt,
|
||||
'costCpu' => $passwordCpu,
|
||||
@@ -331,7 +346,7 @@ App::post('/v1/users/scrypt')
|
||||
'length' => $passwordLength
|
||||
];
|
||||
|
||||
$user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents);
|
||||
$user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -363,8 +378,9 @@ App::post('/v1/users/scrypt-modified')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
$user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents);
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
|
||||
$user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -846,7 +862,8 @@ App::patch('/v1/users/:userId/password')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $password, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $password, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
|
||||
|
||||
$user = $dbForProject->getDocument('users', $userId);
|
||||
|
||||
@@ -861,6 +878,8 @@ App::patch('/v1/users/:userId/password')
|
||||
}
|
||||
}
|
||||
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
|
||||
|
||||
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
||||
|
||||
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||
@@ -1089,7 +1108,7 @@ App::delete('/v1/users/:userId/sessions/:sessionId')
|
||||
}
|
||||
|
||||
$dbForProject->deleteDocument('sessions', $session->getId());
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
@@ -1132,7 +1151,7 @@ App::delete('/v1/users/:userId/sessions')
|
||||
//TODO: fix this
|
||||
}
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
@@ -1184,7 +1203,7 @@ App::delete('/v1/users/:userId')
|
||||
});
|
||||
|
||||
App::delete('/v1/users/identities/:identityId')
|
||||
->desc('Delete Identity')
|
||||
->desc('Delete identity')
|
||||
->groups(['api', 'users'])
|
||||
->label('event', 'users.[userId].identities.[identityId].delete')
|
||||
->label('scope', 'users.write')
|
||||
@@ -1199,7 +1218,8 @@ App::delete('/v1/users/identities/:identityId')
|
||||
->param('identityId', '', new UID(), 'Identity ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $identityId, Response $response, Database $dbForProject) {
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $identityId, Response $response, Database $dbForProject, Event $queueForEvents) {
|
||||
|
||||
$identity = $dbForProject->getDocument('identities', $identityId);
|
||||
|
||||
@@ -1209,6 +1229,11 @@ App::delete('/v1/users/identities/:identityId')
|
||||
|
||||
$dbForProject->deleteDocument('identities', $identityId);
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $identity->getAttribute('userId'))
|
||||
->setParam('identityId', $identity->getId())
|
||||
->setPayload($response->output($identity, Response::MODEL_IDENTITY));
|
||||
|
||||
return $response->noContent();
|
||||
});
|
||||
|
||||
|
||||
+278
-105
@@ -9,9 +9,8 @@ use Utopia\Logger\Logger;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Logger\Log\User;
|
||||
use Swoole\Http\Request as SwooleRequest;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Pools\Group;
|
||||
use Appwrite\Utopia\Request;
|
||||
use MaxMind\Db\Reader;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\View;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
@@ -19,6 +18,8 @@ use Utopia\Config\Config;
|
||||
use Utopia\Domains\Domain;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Usage;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\Utopia\Response\Filters\V11 as ResponseV11;
|
||||
use Appwrite\Utopia\Response\Filters\V12 as ResponseV12;
|
||||
@@ -31,6 +32,7 @@ use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Validator\Hostname;
|
||||
use Appwrite\Utopia\Request\Filters\V12 as RequestV12;
|
||||
@@ -38,6 +40,7 @@ use Appwrite\Utopia\Request\Filters\V13 as RequestV13;
|
||||
use Appwrite\Utopia\Request\Filters\V14 as RequestV14;
|
||||
use Appwrite\Utopia\Request\Filters\V15 as RequestV15;
|
||||
use Appwrite\Utopia\Request\Filters\V16 as RequestV16;
|
||||
use Executor\Executor;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
@@ -45,7 +48,7 @@ Config::setParam('domainVerification', false);
|
||||
Config::setParam('cookieDomain', 'localhost');
|
||||
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
|
||||
|
||||
function router(App $utopia, Database $dbForConsole, SwooleRequest $swooleRequest, Request $request, Response $response)
|
||||
function router(App $utopia, Database $dbForConsole, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, Usage $queueForUsage, Reader $geodb)
|
||||
{
|
||||
$utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml');
|
||||
|
||||
@@ -117,59 +120,218 @@ function router(App $utopia, Database $dbForConsole, SwooleRequest $swooleReques
|
||||
$path .= '?' . $query;
|
||||
}
|
||||
|
||||
|
||||
$body = $swooleRequest->getContent() ?? '';
|
||||
$method = $swooleRequest->server['request_method'];
|
||||
|
||||
$requestHeaders = $request->getHeaders();
|
||||
|
||||
$body = \json_encode([
|
||||
'async' => false,
|
||||
'body' => $swooleRequest->getContent() ?? '',
|
||||
'method' => $swooleRequest->server['request_method'],
|
||||
'path' => $path,
|
||||
'headers' => $requestHeaders
|
||||
$project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId));
|
||||
|
||||
$dbForProject = $getProjectDB($project);
|
||||
|
||||
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
|
||||
|
||||
if ($function->isEmpty() || !$function->getAttribute('enabled')) {
|
||||
throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$version = $function->getAttribute('version', 'v2');
|
||||
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
|
||||
|
||||
$runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null;
|
||||
|
||||
if (\is_null($runtime)) {
|
||||
throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported');
|
||||
}
|
||||
|
||||
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', '')));
|
||||
|
||||
if ($deployment->getAttribute('resourceId') !== $function->getId()) {
|
||||
throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function');
|
||||
}
|
||||
|
||||
if ($deployment->isEmpty()) {
|
||||
throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function');
|
||||
}
|
||||
|
||||
/** Check if build has completed */
|
||||
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
|
||||
if ($build->isEmpty()) {
|
||||
throw new AppwriteException(AppwriteException::BUILD_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($build->getAttribute('status') !== 'ready') {
|
||||
throw new AppwriteException(AppwriteException::BUILD_NOT_READY);
|
||||
}
|
||||
|
||||
$permissions = $function->getAttribute('execute');
|
||||
|
||||
if (!(\in_array('any', $permissions)) && (\in_array('guests', $permissions))) {
|
||||
throw new AppwriteException(AppwriteException::USER_UNAUTHORIZED, 'To execute function using domain, execute permissions must include "any" or "guests"');
|
||||
}
|
||||
|
||||
$headers = \array_merge([], $requestHeaders);
|
||||
$headers['x-appwrite-trigger'] = 'http';
|
||||
$headers['x-appwrite-user-id'] = '';
|
||||
$headers['x-appwrite-user-jwt'] = '';
|
||||
$headers['x-appwrite-country-code'] = '';
|
||||
$headers['x-appwrite-continent-code'] = '';
|
||||
$headers['x-appwrite-continent-eu'] = 'false';
|
||||
|
||||
$ip = $headers['x-real-ip'] ?? '';
|
||||
if (!empty($ip)) {
|
||||
$record = $geodb->get($ip);
|
||||
|
||||
if ($record) {
|
||||
$eu = Config::getParam('locale-eu');
|
||||
|
||||
$headers['x-appwrite-country-code'] = $record['country']['iso_code'] ?? '';
|
||||
$headers['x-appwrite-continent-code'] = $record['continent']['code'] ?? '';
|
||||
$headers['x-appwrite-continent-eu'] = (\in_array($record['country']['iso_code'], $eu)) ? 'true' : 'false';
|
||||
}
|
||||
}
|
||||
|
||||
$headersFiltered = [];
|
||||
foreach ($headers as $key => $value) {
|
||||
if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) {
|
||||
$headersFiltered[] = ['name' => $key, 'value' => $value];
|
||||
}
|
||||
}
|
||||
|
||||
$executionId = ID::unique();
|
||||
|
||||
$execution = new Document([
|
||||
'$id' => $executionId,
|
||||
'$permissions' => [],
|
||||
'functionInternalId' => $function->getInternalId(),
|
||||
'functionId' => $function->getId(),
|
||||
'deploymentInternalId' => $deployment->getInternalId(),
|
||||
'deploymentId' => $deployment->getId(),
|
||||
'trigger' => 'http', // http / schedule / event
|
||||
'status' => 'processing', // waiting / processing / completed / failed
|
||||
'responseStatusCode' => 0,
|
||||
'responseHeaders' => [],
|
||||
'requestPath' => $path,
|
||||
'requestMethod' => $method,
|
||||
'requestHeaders' => $headersFiltered,
|
||||
'errors' => '',
|
||||
'logs' => '',
|
||||
'duration' => 0.0,
|
||||
'search' => implode(' ', [$functionId, $executionId]),
|
||||
]);
|
||||
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'Content-Length: ' . \strlen($body),
|
||||
'X-Appwrite-Project: ' . $projectId
|
||||
];
|
||||
$queueForEvents
|
||||
->setParam('functionId', $function->getId())
|
||||
->setParam('executionId', $execution->getId())
|
||||
->setContext('function', $function);
|
||||
|
||||
$ch = \curl_init();
|
||||
\curl_setopt($ch, CURLOPT_URL, "http://localhost/v1/functions/{$functionId}/executions");
|
||||
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
|
||||
\curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
\curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
// \curl_setopt($ch, CURLOPT_HEADER, true);
|
||||
\curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
||||
\curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
$durationStart = \microtime(true);
|
||||
|
||||
$executionResponse = \curl_exec($ch);
|
||||
$statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = \curl_error($ch);
|
||||
$errNo = \curl_errno($ch);
|
||||
$vars = [];
|
||||
|
||||
\curl_close($ch);
|
||||
|
||||
if ($errNo !== 0) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ARGUMENT_INVALID, "Internal error: " . $error);
|
||||
// V2 vars
|
||||
if ($version === 'v2') {
|
||||
$vars = \array_merge($vars, [
|
||||
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
|
||||
'APPWRITE_FUNCTION_DATA' => $body ?? '',
|
||||
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
|
||||
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
|
||||
]);
|
||||
}
|
||||
|
||||
if ($statusCode >= 400) {
|
||||
$error = \json_decode($executionResponse, true)['message'];
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ARGUMENT_INVALID, "Execution error: " . $error);
|
||||
// Shared vars
|
||||
foreach ($function->getAttribute('varsProject', []) as $var) {
|
||||
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
|
||||
}
|
||||
|
||||
$execution = \json_decode($executionResponse, true);
|
||||
// Function vars
|
||||
foreach ($function->getAttribute('vars', []) as $var) {
|
||||
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
|
||||
}
|
||||
|
||||
$contentType = 'text/plain';
|
||||
foreach ($execution['responseHeaders'] as $header) {
|
||||
if (\strtolower($header['name']) === 'content-type') {
|
||||
$contentType = $header['value'];
|
||||
// Appwrite vars
|
||||
$vars = \array_merge($vars, [
|
||||
'APPWRITE_FUNCTION_ID' => $functionId,
|
||||
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name'),
|
||||
'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),
|
||||
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
|
||||
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
|
||||
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
|
||||
]);
|
||||
|
||||
/** Execute function */
|
||||
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
|
||||
try {
|
||||
$version = $function->getAttribute('version', 'v2');
|
||||
$command = $runtime['startCommand'];
|
||||
$command = $version === 'v2' ? '' : 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $command . '"';
|
||||
$executionResponse = $executor->createExecution(
|
||||
projectId: $project->getId(),
|
||||
deploymentId: $deployment->getId(),
|
||||
body: \strlen($body) > 0 ? $body : null,
|
||||
variables: $vars,
|
||||
timeout: $function->getAttribute('timeout', 0),
|
||||
image: $runtime['image'],
|
||||
source: $build->getAttribute('path', ''),
|
||||
entrypoint: $deployment->getAttribute('entrypoint', ''),
|
||||
version: $version,
|
||||
path: $path,
|
||||
method: $method,
|
||||
headers: $headers,
|
||||
runtimeEntrypoint: $command,
|
||||
requestTimeout: 30
|
||||
);
|
||||
|
||||
$headersFiltered = [];
|
||||
foreach ($executionResponse['headers'] as $key => $value) {
|
||||
if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_RESPONSE)) {
|
||||
$headersFiltered[] = ['name' => $key, 'value' => $value];
|
||||
}
|
||||
}
|
||||
|
||||
$response->setHeader($header['name'], $header['value']);
|
||||
/** Update execution status */
|
||||
$status = $executionResponse['statusCode'] >= 400 ? 'failed' : 'completed';
|
||||
$execution->setAttribute('status', $status);
|
||||
$execution->setAttribute('responseStatusCode', $executionResponse['statusCode']);
|
||||
$execution->setAttribute('responseHeaders', $headersFiltered);
|
||||
$execution->setAttribute('logs', $executionResponse['logs']);
|
||||
$execution->setAttribute('errors', $executionResponse['errors']);
|
||||
$execution->setAttribute('duration', $executionResponse['duration']);
|
||||
} catch (\Throwable $th) {
|
||||
$durationEnd = \microtime(true);
|
||||
|
||||
$execution
|
||||
->setAttribute('duration', $durationEnd - $durationStart)
|
||||
->setAttribute('status', 'failed')
|
||||
->setAttribute('responseStatusCode', 500)
|
||||
->setAttribute('errors', $th->getMessage() . '\nError Code: ' . $th->getCode());
|
||||
Console::error($th->getMessage());
|
||||
} finally {
|
||||
$queueForUsage
|
||||
->addMetric(METRIC_EXECUTIONS, 1)
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1)
|
||||
->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function
|
||||
;
|
||||
}
|
||||
|
||||
if ($function->getAttribute('logging')) {
|
||||
/** @var Document $execution */
|
||||
$execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution));
|
||||
}
|
||||
|
||||
$execution->setAttribute('logs', '');
|
||||
$execution->setAttribute('errors', '');
|
||||
|
||||
$headers = [];
|
||||
foreach (($executionResponse['headers'] ?? []) as $key => $value) {
|
||||
$headers[] = ['name' => $key, 'value' => $value];
|
||||
}
|
||||
|
||||
$execution->setAttribute('responseBody', $executionResponse['body'] ?? '');
|
||||
$execution->setAttribute('responseHeaders', $headers);
|
||||
|
||||
$body = $execution['responseBody'] ?? '';
|
||||
|
||||
$encodingKey = \array_search('x-open-runtimes-encoding', \array_column($execution['responseHeaders'], 'name'));
|
||||
@@ -179,6 +341,15 @@ function router(App $utopia, Database $dbForConsole, SwooleRequest $swooleReques
|
||||
}
|
||||
}
|
||||
|
||||
$contentType = 'text/plain';
|
||||
foreach ($execution['responseHeaders'] as $header) {
|
||||
if (\strtolower($header['name']) === 'content-type') {
|
||||
$contentType = $header['value'];
|
||||
}
|
||||
|
||||
$response->setHeader($header['name'], $header['value']);
|
||||
}
|
||||
|
||||
$response
|
||||
->setContentType($contentType)
|
||||
->setStatusCode($execution['responseStatusCode'] ?? 200)
|
||||
@@ -205,13 +376,17 @@ App::init()
|
||||
->inject('console')
|
||||
->inject('project')
|
||||
->inject('dbForConsole')
|
||||
->inject('getProjectDB')
|
||||
->inject('user')
|
||||
->inject('locale')
|
||||
->inject('localeCodes')
|
||||
->inject('clients')
|
||||
->inject('servers')
|
||||
->inject('queueForCertificates')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $localeCodes, array $clients, array $servers, Certificate $queueForCertificates) {
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForUsage')
|
||||
->inject('geodb')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, callable $getProjectDB, Document $user, Locale $locale, array $localeCodes, array $clients, array $servers, Certificate $queueForCertificates, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) {
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
@@ -220,7 +395,7 @@ App::init()
|
||||
$mainDomain = App::getEnv('_APP_DOMAIN', '');
|
||||
// Only run Router when external domain
|
||||
if ($host !== $mainDomain) {
|
||||
if (router($utopia, $dbForConsole, $swooleRequest, $request, $response)) {
|
||||
if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -232,7 +407,9 @@ App::init()
|
||||
Request::setRoute($route);
|
||||
|
||||
if ($route === null) {
|
||||
return $response->setStatusCode(404)->send('Not Found');
|
||||
return $response
|
||||
->setStatusCode(404)
|
||||
->send('Not Found');
|
||||
}
|
||||
|
||||
$requestFormat = $request->getHeader('x-appwrite-response-format', App::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
|
||||
@@ -425,7 +602,7 @@ App::init()
|
||||
->addHeader('Server', 'Appwrite')
|
||||
->addHeader('X-Content-Type-Options', 'nosniff')
|
||||
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
|
||||
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma')
|
||||
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-Appwrite-Share-Tables, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma')
|
||||
->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies')
|
||||
->addHeader('Access-Control-Allow-Origin', $refDomain)
|
||||
->addHeader('Access-Control-Allow-Credentials', 'true');
|
||||
@@ -511,7 +688,7 @@ App::init()
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCCESS)) > $accessedAt) {
|
||||
$key->setAttribute('accessedAt', DateTime::now());
|
||||
$dbForConsole->updateDocument('keys', $key->getId(), $key);
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
$dbForConsole->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
|
||||
$sdkValidator = new WhiteList($servers, true);
|
||||
@@ -525,7 +702,7 @@ App::init()
|
||||
/** Update access time as well */
|
||||
$key->setAttribute('accessedAt', Datetime::now());
|
||||
$dbForConsole->updateDocument('keys', $key->getId(), $key);
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
$dbForConsole->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -571,7 +748,11 @@ App::options()
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole) {
|
||||
->inject('getProjectDB')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForUsage')
|
||||
->inject('geodb')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) {
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
@@ -579,7 +760,7 @@ App::options()
|
||||
$mainDomain = App::getEnv('_APP_DOMAIN', '');
|
||||
// Only run Router when external domain
|
||||
if ($host !== $mainDomain) {
|
||||
if (router($utopia, $dbForConsole, $swooleRequest, $request, $response)) {
|
||||
if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -589,7 +770,7 @@ App::options()
|
||||
$response
|
||||
->addHeader('Server', 'Appwrite')
|
||||
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
|
||||
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Fallback-Cookies')
|
||||
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-Appwrite-Share-Tables, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Fallback-Cookies')
|
||||
->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies')
|
||||
->addHeader('Access-Control-Allow-Origin', $origin)
|
||||
->addHeader('Access-Control-Allow-Credentials', 'true')
|
||||
@@ -603,66 +784,58 @@ App::error()
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('logger')
|
||||
->inject('loggerBreadcrumbs')
|
||||
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, array $loggerBreadcrumbs) {
|
||||
|
||||
->inject('log')
|
||||
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log) {
|
||||
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
$route = $utopia->getRoute();
|
||||
$publish = true;
|
||||
|
||||
if ($error instanceof AppwriteException) {
|
||||
$publish = $error->isPublishable();
|
||||
} else {
|
||||
$publish = $error->getCode() === 0 || $error->getCode() >= 500;
|
||||
}
|
||||
|
||||
if ($logger && $publish) {
|
||||
if ($error->getCode() >= 500 || $error->getCode() === 0) {
|
||||
try {
|
||||
/** @var Utopia\Database\Document $user */
|
||||
$user = $utopia->getResource('user');
|
||||
} catch (\Throwable $th) {
|
||||
// All good, user is optional information for logger
|
||||
}
|
||||
|
||||
$log = new Utopia\Logger\Log();
|
||||
|
||||
if (isset($user) && !$user->isEmpty()) {
|
||||
$log->setUser(new User($user->getId()));
|
||||
}
|
||||
|
||||
$log->setNamespace("http");
|
||||
$log->setServer(\gethostname());
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
|
||||
$log->addTag('database', $project->getAttribute('database', 'console'));
|
||||
$log->addTag('method', $route->getMethod());
|
||||
$log->addTag('url', $route->getPath());
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('projectId', $project->getId());
|
||||
$log->addTag('hostname', $request->getHostname());
|
||||
$log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', '')));
|
||||
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
$log->addExtra('detailedTrace', $error->getTrace());
|
||||
$log->addExtra('roles', Authorization::getRoles());
|
||||
|
||||
$action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD");
|
||||
$log->setAction($action);
|
||||
|
||||
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
foreach ($loggerBreadcrumbs as $loggerBreadcrumb) {
|
||||
$log->addBreadcrumb($loggerBreadcrumb);
|
||||
}
|
||||
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Log pushed with status code: ' . $responseCode);
|
||||
if ($logger && ($publish || $error->getCode() === 0)) {
|
||||
try {
|
||||
/** @var Utopia\Database\Document $user */
|
||||
$user = $utopia->getResource('user');
|
||||
} catch (\Throwable $th) {
|
||||
// All good, user is optional information for logger
|
||||
}
|
||||
|
||||
if (isset($user) && !$user->isEmpty()) {
|
||||
$log->setUser(new User($user->getId()));
|
||||
}
|
||||
|
||||
$log->setNamespace("http");
|
||||
$log->setServer(\gethostname());
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
|
||||
$log->addTag('database', $project->getAttribute('database', 'console'));
|
||||
$log->addTag('method', $route->getMethod());
|
||||
$log->addTag('url', $route->getPath());
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('projectId', $project->getId());
|
||||
$log->addTag('hostname', $request->getHostname());
|
||||
$log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', '')));
|
||||
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
$log->addExtra('detailedTrace', $error->getTrace());
|
||||
$log->addExtra('roles', Authorization::getRoles());
|
||||
|
||||
$action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD");
|
||||
$log->setAction($action);
|
||||
|
||||
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Log pushed with status code: ' . $responseCode);
|
||||
}
|
||||
|
||||
$code = $error->getCode();
|
||||
@@ -765,10 +938,10 @@ App::error()
|
||||
->setParam('development', App::isDevelopment())
|
||||
->setParam('projectName', $project->getAttribute('name'))
|
||||
->setParam('projectURL', $project->getAttribute('url'))
|
||||
->setParam('message', $error->getMessage())
|
||||
->setParam('type', $type)
|
||||
->setParam('code', $code)
|
||||
->setParam('trace', $trace);
|
||||
->setParam('message', $output['message'] ?? '')
|
||||
->setParam('type', $output['type'] ?? '')
|
||||
->setParam('code', $output['code'] ?? '')
|
||||
->setParam('trace', $output['trace'] ?? []);
|
||||
|
||||
$response->html($layout->render());
|
||||
}
|
||||
|
||||
@@ -177,6 +177,7 @@ App::init()
|
||||
$end = $request->getContentRangeEnd();
|
||||
$timeLimit = new TimeLimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600), $dbForProject);
|
||||
$timeLimit
|
||||
->setParam('{projectId}', $project->getId())
|
||||
->setParam('{userId}', $user->getId())
|
||||
->setParam('{userAgent}', $request->getUserAgent(''))
|
||||
->setParam('{ip}', $request->getIP())
|
||||
@@ -335,7 +336,7 @@ App::init()
|
||||
break;
|
||||
|
||||
case 'magic-url':
|
||||
if ($project->getAttribute('usersAuthMagicURL', true) === false) {
|
||||
if (($auths['usersAuthMagicURL'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Magic URL authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
@@ -346,6 +347,12 @@ App::init()
|
||||
}
|
||||
break;
|
||||
|
||||
case 'phone':
|
||||
if (($auths['phone'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Phone authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'invites':
|
||||
if (($auths['invites'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Invites authentication is disabled for this project');
|
||||
@@ -359,7 +366,7 @@ App::init()
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Unsupported authentication route');
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Unsupported authentication type: ' . $route->getLabel('auth.type', ''));
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -400,7 +407,7 @@ App::shutdown()
|
||||
$session = array_shift($sessions);
|
||||
$dbForProject->deleteDocument('sessions', $session->getId());
|
||||
}
|
||||
$dbForProject->deleteCachedDocument('users', $userId);
|
||||
$dbForProject->purgeCachedDocument('users', $userId);
|
||||
});
|
||||
|
||||
App::shutdown()
|
||||
@@ -592,6 +599,22 @@ App::shutdown()
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user last activity
|
||||
*/
|
||||
if (!$user->isEmpty()) {
|
||||
$accessedAt = $user->getAttribute('accessedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCCESS)) > $accessedAt) {
|
||||
$user->setAttribute('accessedAt', DateTime::now());
|
||||
|
||||
if (APP_MODE_ADMIN !== $mode) {
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
} else {
|
||||
$dbForConsole->updateDocument('users', $user->getId(), $user);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
App::init()
|
||||
|
||||
@@ -32,7 +32,7 @@ App::init()
|
||||
break;
|
||||
|
||||
case 'magic-url':
|
||||
if ($project->getAttribute('usersAuthMagicURL', true) === false) {
|
||||
if (($auths['usersAuthMagicURL'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Magic URL authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
@@ -43,6 +43,12 @@ App::init()
|
||||
}
|
||||
break;
|
||||
|
||||
case 'phone':
|
||||
if (($auths['phone'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Phone authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'invites':
|
||||
if (($auths['invites'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Invites authentication is disabled for this project');
|
||||
|
||||
+2
-7
@@ -147,7 +147,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
|
||||
$dbForConsole->createCollection($key, $attributes, $indexes);
|
||||
}
|
||||
|
||||
if ($dbForConsole->getDocument('buckets', 'default')->isEmpty() && !$dbForConsole->exists($dbForConsole->getDefaultDatabase(), 'bucket_1')) {
|
||||
if ($dbForConsole->getDocument('buckets', 'default')->isEmpty() && !$dbForConsole->exists($dbForConsole->getDatabase(), 'bucket_1')) {
|
||||
Console::success('[Setup] - Creating default bucket...');
|
||||
$dbForConsole->createDocument('buckets', new Document([
|
||||
'$id' => ID::custom('default'),
|
||||
@@ -263,10 +263,9 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
|
||||
// All good, user is optional information for logger
|
||||
}
|
||||
|
||||
$loggerBreadcrumbs = $app->getResource("loggerBreadcrumbs");
|
||||
$route = $app->getRoute();
|
||||
|
||||
$log = new Utopia\Logger\Log();
|
||||
$log = $app->getResource("log");
|
||||
|
||||
if (isset($user) && !$user->isEmpty()) {
|
||||
$log->setUser(new User($user->getId()));
|
||||
@@ -298,10 +297,6 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
|
||||
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
foreach ($loggerBreadcrumbs as $loggerBreadcrumb) {
|
||||
$log->addBreadcrumb($loggerBreadcrumb);
|
||||
}
|
||||
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Log pushed with status code: ' . $responseCode);
|
||||
}
|
||||
|
||||
+170
-155
@@ -12,11 +12,11 @@ if (\file_exists(__DIR__ . '/../vendor/autoload.php')) {
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
}
|
||||
|
||||
ini_set('memory_limit', '512M');
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
ini_set('default_socket_timeout', -1);
|
||||
error_reporting(E_ALL);
|
||||
\ini_set('memory_limit', '512M');
|
||||
\ini_set('display_errors', 1);
|
||||
\ini_set('display_startup_errors', 1);
|
||||
\ini_set('default_socket_timeout', -1);
|
||||
\error_reporting(E_ALL);
|
||||
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Usage;
|
||||
@@ -34,6 +34,7 @@ use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\OpenSSL\OpenSSL;
|
||||
use Appwrite\URL\URL as AppwriteURL;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Cache\Adapter\Redis as RedisCache;
|
||||
use Utopia\Cache\Cache;
|
||||
@@ -76,11 +77,13 @@ use Appwrite\Hooks\Hooks;
|
||||
use MaxMind\Db\Reader;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Swoole\Database\PDOProxy;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Queue;
|
||||
use Utopia\Queue\Connection;
|
||||
use Utopia\Storage\Storage;
|
||||
use Utopia\VCS\Adapter\Git\GitHub as VcsGitHub;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Hostname;
|
||||
use Utopia\Validator\IP;
|
||||
use Utopia\Validator\URL;
|
||||
use Utopia\Validator\WhiteList;
|
||||
@@ -110,7 +113,7 @@ const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return
|
||||
const APP_KEY_ACCCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_USER_ACCCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_BUSTER = 328;
|
||||
const APP_CACHE_BUSTER = 331;
|
||||
const APP_VERSION_STABLE = '1.4.13';
|
||||
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
|
||||
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
|
||||
@@ -140,7 +143,8 @@ const APP_SOCIAL_DEV = 'https://dev.to/appwrite';
|
||||
const APP_SOCIAL_STACKSHARE = 'https://stackshare.io/appwrite';
|
||||
const APP_SOCIAL_YOUTUBE = 'https://www.youtube.com/c/appwrite?sub_confirmation=1';
|
||||
const APP_HOSTNAME_INTERNAL = 'appwrite';
|
||||
// Database Reconnect
|
||||
// Database
|
||||
const DATABASE_SHARED_TABLES = 'database_db_self_hosted_shared_tables';
|
||||
const DATABASE_RECONNECT_SLEEP = 2;
|
||||
const DATABASE_RECONNECT_MAX_ATTEMPTS = 10;
|
||||
// Database Worker Types
|
||||
@@ -174,10 +178,6 @@ const DELETE_TYPE_SESSIONS = 'sessions';
|
||||
const DELETE_TYPE_CACHE_BY_TIMESTAMP = 'cacheByTimeStamp';
|
||||
const DELETE_TYPE_CACHE_BY_RESOURCE = 'cacheByResource';
|
||||
const DELETE_TYPE_SCHEDULES = 'schedules';
|
||||
// Compression type
|
||||
const COMPRESSION_TYPE_NONE = 'none';
|
||||
const COMPRESSION_TYPE_GZIP = 'gzip';
|
||||
const COMPRESSION_TYPE_ZSTD = 'zstd';
|
||||
// Mail Types
|
||||
const MAIL_TYPE_VERIFICATION = 'verification';
|
||||
const MAIL_TYPE_MAGIC_SESSION = 'magicSession';
|
||||
@@ -197,6 +197,7 @@ const FUNCTION_ALLOWLIST_HEADERS_RESPONSE = ['content-type', 'content-length'];
|
||||
// Usage metrics
|
||||
const METRIC_TEAMS = 'teams';
|
||||
const METRIC_USERS = 'users';
|
||||
const METRIC_MESSAGES = 'messages';
|
||||
const METRIC_SESSIONS = 'sessions';
|
||||
const METRIC_DATABASES = 'databases';
|
||||
const METRIC_COLLECTIONS = 'collections';
|
||||
@@ -655,14 +656,13 @@ $register->set('pools', function () {
|
||||
|
||||
foreach ($connections as $key => $connection) {
|
||||
$type = $connection['type'] ?? '';
|
||||
$dsns = $connection['dsns'] ?? '';
|
||||
$multipe = $connection['multiple'] ?? false;
|
||||
$multiple = $connection['multiple'] ?? false;
|
||||
$schemes = $connection['schemes'] ?? [];
|
||||
$config = [];
|
||||
$dsns = explode(',', $connection['dsns'] ?? '');
|
||||
foreach ($dsns as &$dsn) {
|
||||
$dsn = explode('=', $dsn);
|
||||
$name = ($multipe) ? $key . '_' . $dsn[0] : $key;
|
||||
$name = ($multiple) ? $key . '_' . $dsn[0] : $key;
|
||||
$dsn = $dsn[1] ?? '';
|
||||
$config[] = $name;
|
||||
if (empty($dsn)) {
|
||||
@@ -685,47 +685,38 @@ $register->set('pools', function () {
|
||||
/**
|
||||
* Get Resource
|
||||
*
|
||||
* Creation could be reused accross connection types like database, cache, queue, etc.
|
||||
* Creation could be reused across connection types like database, cache, queue, etc.
|
||||
*
|
||||
* Resource assignment to an adapter will happen below.
|
||||
*/
|
||||
switch ($dsnScheme) {
|
||||
case 'mysql':
|
||||
case 'mariadb':
|
||||
$resource = function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) {
|
||||
return new PDOProxy(function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) {
|
||||
return new PDO("mysql:host={$dsnHost};port={$dsnPort};dbname={$dsnDatabase};charset=utf8mb4", $dsnUser, $dsnPass, array(
|
||||
PDO::ATTR_TIMEOUT => 3, // Seconds
|
||||
PDO::ATTR_PERSISTENT => true,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_ERRMODE => App::isDevelopment() ? PDO::ERRMODE_WARNING : PDO::ERRMODE_SILENT, // If in production mode, warnings are not displayed
|
||||
PDO::ATTR_EMULATE_PREPARES => true,
|
||||
PDO::ATTR_STRINGIFY_FETCHES => true
|
||||
));
|
||||
});
|
||||
};
|
||||
break;
|
||||
case 'redis':
|
||||
$resource = function () use ($dsnHost, $dsnPort, $dsnPass) {
|
||||
$redis = new Redis();
|
||||
@$redis->pconnect($dsnHost, (int)$dsnPort);
|
||||
if ($dsnPass) {
|
||||
$redis->auth($dsnPass);
|
||||
}
|
||||
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
|
||||
$resource = match ($dsnScheme) {
|
||||
'mysql',
|
||||
'mariadb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) {
|
||||
return new PDOProxy(function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) {
|
||||
return new PDO("mysql:host={$dsnHost};port={$dsnPort};dbname={$dsnDatabase};charset=utf8mb4", $dsnUser, $dsnPass, array(
|
||||
PDO::ATTR_TIMEOUT => 3, // Seconds
|
||||
PDO::ATTR_PERSISTENT => true,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => true,
|
||||
PDO::ATTR_STRINGIFY_FETCHES => true
|
||||
));
|
||||
});
|
||||
},
|
||||
'redis' => function () use ($dsnHost, $dsnPort, $dsnPass) {
|
||||
$redis = new Redis();
|
||||
@$redis->pconnect($dsnHost, (int)$dsnPort);
|
||||
if ($dsnPass) {
|
||||
$redis->auth($dsnPass);
|
||||
}
|
||||
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
|
||||
|
||||
return $redis;
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Invalid scheme");
|
||||
break;
|
||||
}
|
||||
return $redis;
|
||||
},
|
||||
default => throw new Exception(Exception::GENERAL_SERVER_ERROR, "Invalid scheme"),
|
||||
};
|
||||
|
||||
$pool = new Pool($name, $poolSize, function () use ($type, $resource, $dsn) {
|
||||
// Get Adapter
|
||||
$adapter = null;
|
||||
switch ($type) {
|
||||
case 'database':
|
||||
$adapter = match ($dsn->getScheme()) {
|
||||
@@ -734,7 +725,7 @@ $register->set('pools', function () {
|
||||
default => null
|
||||
};
|
||||
|
||||
$adapter->setDefaultDatabase($dsn->getPath());
|
||||
$adapter->setDatabase($dsn->getPath());
|
||||
break;
|
||||
case 'pubsub':
|
||||
$adapter = $resource();
|
||||
@@ -754,7 +745,6 @@ $register->set('pools', function () {
|
||||
|
||||
default:
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Server error: Missing adapter implementation.");
|
||||
break;
|
||||
}
|
||||
|
||||
return $adapter;
|
||||
@@ -771,22 +761,22 @@ $register->set('pools', function () {
|
||||
|
||||
$register->set('db', function () {
|
||||
// This is usually for our workers or CLI commands scope
|
||||
$dbHost = App::getEnv('_APP_DB_HOST', '');
|
||||
$dbPort = App::getEnv('_APP_DB_PORT', '');
|
||||
$dbUser = App::getEnv('_APP_DB_USER', '');
|
||||
$dbPass = App::getEnv('_APP_DB_PASS', '');
|
||||
$dbScheme = App::getEnv('_APP_DB_SCHEMA', '');
|
||||
$dbHost = App::getEnv('_APP_DB_HOST', '');
|
||||
$dbPort = App::getEnv('_APP_DB_PORT', '');
|
||||
$dbUser = App::getEnv('_APP_DB_USER', '');
|
||||
$dbPass = App::getEnv('_APP_DB_PASS', '');
|
||||
$dbScheme = App::getEnv('_APP_DB_SCHEMA', '');
|
||||
|
||||
$pdo = new PDO("mysql:host={$dbHost};port={$dbPort};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array(
|
||||
PDO::ATTR_TIMEOUT => 3, // Seconds
|
||||
PDO::ATTR_PERSISTENT => true,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_EMULATE_PREPARES => true,
|
||||
PDO::ATTR_STRINGIFY_FETCHES => true,
|
||||
));
|
||||
$pdo = new PDO("mysql:host={$dbHost};port={$dbPort};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array(
|
||||
PDO::ATTR_TIMEOUT => 3, // Seconds
|
||||
PDO::ATTR_PERSISTENT => true,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_EMULATE_PREPARES => true,
|
||||
PDO::ATTR_STRINGIFY_FETCHES => true,
|
||||
));
|
||||
|
||||
return $pdo;
|
||||
return $pdo;
|
||||
});
|
||||
|
||||
$register->set('smtp', function () {
|
||||
@@ -867,6 +857,7 @@ foreach ($locales as $locale) {
|
||||
]);
|
||||
|
||||
// Runtime Execution
|
||||
App::setResource('log', fn() => new Log());
|
||||
App::setResource('logger', function ($register) {
|
||||
return $register->get('logger');
|
||||
}, ['register']);
|
||||
@@ -875,10 +866,6 @@ App::setResource('hooks', function ($register) {
|
||||
return $register->get('hooks');
|
||||
}, ['register']);
|
||||
|
||||
App::setResource('loggerBreadcrumbs', function () {
|
||||
return [];
|
||||
});
|
||||
|
||||
App::setResource('register', fn() => $register);
|
||||
App::setResource('locale', fn() => new Locale(App::getEnv('_APP_LOCALE', 'en')));
|
||||
|
||||
@@ -931,6 +918,21 @@ App::setResource('clients', function ($request, $console, $project) {
|
||||
'hostname' => $request->getHostname(),
|
||||
], Document::SET_TYPE_APPEND);
|
||||
|
||||
$hostnames = explode(',', App::getEnv('_APP_CONSOLE_HOSTNAMES', ''));
|
||||
$validator = new Hostname();
|
||||
foreach ($hostnames as $hostname) {
|
||||
$hostname = trim($hostname);
|
||||
if (!$validator->isValid($hostname)) {
|
||||
continue;
|
||||
}
|
||||
$console->setAttribute('platforms', [
|
||||
'$collection' => ID::custom('platforms'),
|
||||
'type' => Origin::CLIENT_TYPE_WEB,
|
||||
'name' => $hostname,
|
||||
'hostname' => $hostname,
|
||||
], Document::SET_TYPE_APPEND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get All verified client URLs for both console and current projects
|
||||
* + Filter for duplicated entries
|
||||
@@ -939,24 +941,25 @@ App::setResource('clients', function ($request, $console, $project) {
|
||||
fn ($node) => $node['hostname'],
|
||||
\array_filter(
|
||||
$console->getAttribute('platforms', []),
|
||||
fn ($node) => (isset($node['type']) && ($node['type'] === Origin::CLIENT_TYPE_WEB) && isset($node['hostname']) && !empty($node['hostname']))
|
||||
fn ($node) => (isset($node['type']) && ($node['type'] === Origin::CLIENT_TYPE_WEB) && !empty($node['hostname']))
|
||||
)
|
||||
);
|
||||
|
||||
$clients = \array_unique(
|
||||
\array_merge(
|
||||
$clientsConsole,
|
||||
\array_map(
|
||||
fn ($node) => $node['hostname'],
|
||||
\array_filter(
|
||||
$project->getAttribute('platforms', []),
|
||||
fn ($node) => (isset($node['type']) && ($node['type'] === Origin::CLIENT_TYPE_WEB || $node['type'] === Origin::CLIENT_TYPE_FLUTTER_WEB) && isset($node['hostname']) && !empty($node['hostname']))
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
$clients = $clientsConsole;
|
||||
$platforms = $project->getAttribute('platforms', []);
|
||||
|
||||
return $clients;
|
||||
foreach ($platforms as $node) {
|
||||
if (
|
||||
isset($node['type']) &&
|
||||
($node['type'] === Origin::CLIENT_TYPE_WEB ||
|
||||
$node['type'] === Origin::CLIENT_TYPE_FLUTTER_WEB) &&
|
||||
!empty($node['hostname'])
|
||||
) {
|
||||
$clients[] = $node['hostname'];
|
||||
}
|
||||
}
|
||||
|
||||
return \array_unique($clients);
|
||||
}, ['request', 'console', 'project']);
|
||||
|
||||
App::setResource('user', function ($mode, $project, $console, $request, $response, $dbForProject, $dbForConsole) {
|
||||
@@ -1128,11 +1131,22 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForConsole,
|
||||
$database = new Database($dbAdapter, $cache);
|
||||
|
||||
$database
|
||||
->setNamespace('_' . $project->getInternalId())
|
||||
->setMetadata('host', \gethostname())
|
||||
->setMetadata('project', $project->getId())
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS);
|
||||
|
||||
if ($project->getAttribute('database') === DATABASE_SHARED_TABLES) {
|
||||
$database
|
||||
->setShareTables(true)
|
||||
->setTenant($project->getInternalId())
|
||||
->setNamespace('');
|
||||
} else {
|
||||
$database
|
||||
->setShareTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getInternalId());
|
||||
}
|
||||
|
||||
return $database;
|
||||
}, ['pools', 'dbForConsole', 'cache', 'project']);
|
||||
|
||||
@@ -1140,8 +1154,7 @@ App::setResource('dbForConsole', function (Group $pools, Cache $cache) {
|
||||
$dbAdapter = $pools
|
||||
->get('console')
|
||||
->pop()
|
||||
->getResource()
|
||||
;
|
||||
->getResource();
|
||||
|
||||
$database = new Database($dbAdapter, $cache);
|
||||
|
||||
@@ -1157,22 +1170,35 @@ App::setResource('dbForConsole', function (Group $pools, Cache $cache) {
|
||||
App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) {
|
||||
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
|
||||
|
||||
$getProjectDB = function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) {
|
||||
return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForConsole;
|
||||
}
|
||||
|
||||
$databaseName = $project->getAttribute('database');
|
||||
|
||||
if (isset($databases[$databaseName])) {
|
||||
$database = $databases[$databaseName];
|
||||
|
||||
$configure = (function (Database $database) use ($project) {
|
||||
$database
|
||||
->setNamespace('_' . $project->getInternalId())
|
||||
->setMetadata('host', \gethostname())
|
||||
->setMetadata('project', $project->getId())
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS);
|
||||
|
||||
if ($project->getAttribute('database') === DATABASE_SHARED_TABLES) {
|
||||
$database
|
||||
->setShareTables(true)
|
||||
->setTenant($project->getInternalId())
|
||||
->setNamespace('');
|
||||
} else {
|
||||
$database
|
||||
->setShareTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getInternalId());
|
||||
}
|
||||
});
|
||||
|
||||
if (isset($databases[$databaseName])) {
|
||||
$database = $databases[$databaseName];
|
||||
$configure($database);
|
||||
return $database;
|
||||
}
|
||||
|
||||
@@ -1182,19 +1208,10 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole,
|
||||
->getResource();
|
||||
|
||||
$database = new Database($dbAdapter, $cache);
|
||||
|
||||
$databases[$databaseName] = $database;
|
||||
|
||||
$database
|
||||
->setNamespace('_' . $project->getInternalId())
|
||||
->setMetadata('host', \gethostname())
|
||||
->setMetadata('project', $project->getId())
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS);
|
||||
|
||||
$configure($database);
|
||||
return $database;
|
||||
};
|
||||
|
||||
return $getProjectDB;
|
||||
}, ['pools', 'dbForConsole', 'cache']);
|
||||
|
||||
App::setResource('cache', function (Group $pools) {
|
||||
@@ -1251,62 +1268,60 @@ function getDevice($root): Device
|
||||
Console::warning($e->getMessage() . 'Invalid DSN. Defaulting to Local device.');
|
||||
}
|
||||
|
||||
switch ($device) {
|
||||
case Storage::DEVICE_S3:
|
||||
return new S3($root, $accessKey, $accessSecret, $bucket, $region, $acl);
|
||||
case STORAGE::DEVICE_DO_SPACES:
|
||||
return new DOSpaces($root, $accessKey, $accessSecret, $bucket, $region, $acl);
|
||||
case Storage::DEVICE_BACKBLAZE:
|
||||
return new Backblaze($root, $accessKey, $accessSecret, $bucket, $region, $acl);
|
||||
case Storage::DEVICE_LINODE:
|
||||
return new Linode($root, $accessKey, $accessSecret, $bucket, $region, $acl);
|
||||
case Storage::DEVICE_WASABI:
|
||||
return new Wasabi($root, $accessKey, $accessSecret, $bucket, $region, $acl);
|
||||
case Storage::DEVICE_LOCAL:
|
||||
default:
|
||||
return new Local($root);
|
||||
}
|
||||
return match ($device) {
|
||||
Storage::DEVICE_S3 => new S3($root, $accessKey, $accessSecret, $bucket, $region, $acl),
|
||||
Storage::DEVICE_DO_SPACES => new DOSpaces($root, $accessKey, $accessSecret, $bucket, $region, $acl),
|
||||
Storage::DEVICE_BACKBLAZE => new Backblaze($root, $accessKey, $accessSecret, $bucket, $region, $acl),
|
||||
Storage::DEVICE_LINODE => new Linode($root, $accessKey, $accessSecret, $bucket, $region, $acl),
|
||||
Storage::DEVICE_WASABI => new Wasabi($root, $accessKey, $accessSecret, $bucket, $region, $acl),
|
||||
default => new Local($root),
|
||||
};
|
||||
} else {
|
||||
switch (strtolower(App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) ?? '')) {
|
||||
case Storage::DEVICE_LOCAL:
|
||||
default:
|
||||
return new Local($root);
|
||||
case Storage::DEVICE_S3:
|
||||
$s3AccessKey = App::getEnv('_APP_STORAGE_S3_ACCESS_KEY', '');
|
||||
$s3SecretKey = App::getEnv('_APP_STORAGE_S3_SECRET', '');
|
||||
$s3Region = App::getEnv('_APP_STORAGE_S3_REGION', '');
|
||||
$s3Bucket = App::getEnv('_APP_STORAGE_S3_BUCKET', '');
|
||||
$s3Acl = 'private';
|
||||
return new S3($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl);
|
||||
case Storage::DEVICE_DO_SPACES:
|
||||
$doSpacesAccessKey = App::getEnv('_APP_STORAGE_DO_SPACES_ACCESS_KEY', '');
|
||||
$doSpacesSecretKey = App::getEnv('_APP_STORAGE_DO_SPACES_SECRET', '');
|
||||
$doSpacesRegion = App::getEnv('_APP_STORAGE_DO_SPACES_REGION', '');
|
||||
$doSpacesBucket = App::getEnv('_APP_STORAGE_DO_SPACES_BUCKET', '');
|
||||
$doSpacesAcl = 'private';
|
||||
return new DOSpaces($root, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl);
|
||||
case Storage::DEVICE_BACKBLAZE:
|
||||
$backblazeAccessKey = App::getEnv('_APP_STORAGE_BACKBLAZE_ACCESS_KEY', '');
|
||||
$backblazeSecretKey = App::getEnv('_APP_STORAGE_BACKBLAZE_SECRET', '');
|
||||
$backblazeRegion = App::getEnv('_APP_STORAGE_BACKBLAZE_REGION', '');
|
||||
$backblazeBucket = App::getEnv('_APP_STORAGE_BACKBLAZE_BUCKET', '');
|
||||
$backblazeAcl = 'private';
|
||||
return new Backblaze($root, $backblazeAccessKey, $backblazeSecretKey, $backblazeBucket, $backblazeRegion, $backblazeAcl);
|
||||
case Storage::DEVICE_LINODE:
|
||||
$linodeAccessKey = App::getEnv('_APP_STORAGE_LINODE_ACCESS_KEY', '');
|
||||
$linodeSecretKey = App::getEnv('_APP_STORAGE_LINODE_SECRET', '');
|
||||
$linodeRegion = App::getEnv('_APP_STORAGE_LINODE_REGION', '');
|
||||
$linodeBucket = App::getEnv('_APP_STORAGE_LINODE_BUCKET', '');
|
||||
$linodeAcl = 'private';
|
||||
return new Linode($root, $linodeAccessKey, $linodeSecretKey, $linodeBucket, $linodeRegion, $linodeAcl);
|
||||
case Storage::DEVICE_WASABI:
|
||||
$wasabiAccessKey = App::getEnv('_APP_STORAGE_WASABI_ACCESS_KEY', '');
|
||||
$wasabiSecretKey = App::getEnv('_APP_STORAGE_WASABI_SECRET', '');
|
||||
$wasabiRegion = App::getEnv('_APP_STORAGE_WASABI_REGION', '');
|
||||
$wasabiBucket = App::getEnv('_APP_STORAGE_WASABI_BUCKET', '');
|
||||
$wasabiAcl = 'private';
|
||||
return new Wasabi($root, $wasabiAccessKey, $wasabiSecretKey, $wasabiBucket, $wasabiRegion, $wasabiAcl);
|
||||
}
|
||||
$device = strtolower(App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) ?? '');
|
||||
|
||||
return match ($device) {
|
||||
Storage::DEVICE_S3 => new S3(
|
||||
$root,
|
||||
App::getEnv('_APP_STORAGE_S3_ACCESS_KEY', ''),
|
||||
App::getEnv('_APP_STORAGE_S3_SECRET', ''),
|
||||
App::getEnv('_APP_STORAGE_S3_BUCKET', ''),
|
||||
App::getEnv('_APP_STORAGE_S3_REGION', ''),
|
||||
'private'
|
||||
),
|
||||
Storage::DEVICE_DO_SPACES => new DOSpaces(
|
||||
$root,
|
||||
App::getEnv('_APP_STORAGE_DO_SPACES_ACCESS_KEY', ''),
|
||||
App::getEnv('_APP_STORAGE_DO_SPACES_SECRET', ''),
|
||||
App::getEnv('_APP_STORAGE_DO_SPACES_BUCKET', ''),
|
||||
App::getEnv('_APP_STORAGE_DO_SPACES_REGION', ''),
|
||||
'private'
|
||||
),
|
||||
Storage::DEVICE_BACKBLAZE => new Backblaze(
|
||||
$root,
|
||||
App::getEnv('_APP_STORAGE_BACKBLAZE_ACCESS_KEY', ''),
|
||||
App::getEnv('_APP_STORAGE_BACKBLAZE_SECRET', ''),
|
||||
App::getEnv('_APP_STORAGE_BACKBLAZE_BUCKET', ''),
|
||||
App::getEnv('_APP_STORAGE_BACKBLAZE_REGION', ''),
|
||||
'private'
|
||||
),
|
||||
Storage::DEVICE_LINODE => new Linode(
|
||||
$root,
|
||||
App::getEnv('_APP_STORAGE_LINODE_ACCESS_KEY', ''),
|
||||
App::getEnv('_APP_STORAGE_LINODE_SECRET', ''),
|
||||
App::getEnv('_APP_STORAGE_LINODE_BUCKET', ''),
|
||||
App::getEnv('_APP_STORAGE_LINODE_REGION', ''),
|
||||
'private'
|
||||
),
|
||||
Storage::DEVICE_WASABI => new Wasabi(
|
||||
$root,
|
||||
App::getEnv('_APP_STORAGE_WASABI_ACCESS_KEY', ''),
|
||||
App::getEnv('_APP_STORAGE_WASABI_SECRET', ''),
|
||||
App::getEnv('_APP_STORAGE_WASABI_BUCKET', ''),
|
||||
App::getEnv('_APP_STORAGE_WASABI_REGION', ''),
|
||||
'private'
|
||||
),
|
||||
default => new Local($root),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ services:
|
||||
- _APP_CONSOLE_WHITELIST_ROOT
|
||||
- _APP_CONSOLE_WHITELIST_EMAILS
|
||||
- _APP_CONSOLE_WHITELIST_IPS
|
||||
- _APP_CONSOLE_HOSTNAMES
|
||||
- _APP_SYSTEM_EMAIL_NAME
|
||||
- _APP_SYSTEM_EMAIL_ADDRESS
|
||||
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
|
||||
@@ -142,6 +143,7 @@ services:
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_MAINTENANCE_INTERVAL
|
||||
- _APP_MAINTENANCE_DELAY
|
||||
- _APP_MAINTENANCE_RETENTION_EXECUTION
|
||||
- _APP_MAINTENANCE_RETENTION_CACHE
|
||||
- _APP_MAINTENANCE_RETENTION_ABUSE
|
||||
@@ -268,7 +270,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
volumes:
|
||||
volumes:
|
||||
- appwrite-uploads:/storage/uploads:rw
|
||||
- appwrite-cache:/storage/cache:rw
|
||||
- appwrite-functions:/storage/functions:rw
|
||||
@@ -415,7 +417,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
volumes:
|
||||
volumes:
|
||||
- appwrite-config:/storage/config:rw
|
||||
- appwrite-certificates:/storage/certificates:rw
|
||||
environment:
|
||||
@@ -643,7 +645,7 @@ services:
|
||||
- _APP_DB_PASS
|
||||
|
||||
appwrite-assistant:
|
||||
image: appwrite/assistant:0.2.2
|
||||
image: appwrite/assistant:0.3.0
|
||||
container_name: appwrite-assistant
|
||||
<<: *x-logging
|
||||
restart: unless-stopped
|
||||
|
||||
+62
-18
@@ -9,10 +9,12 @@ use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Hamster;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Phone;
|
||||
use Appwrite\Event\Usage;
|
||||
use Appwrite\Event\UsageDump;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
use Swoole\Runtime;
|
||||
use Utopia\App;
|
||||
@@ -61,15 +63,31 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
|
||||
}
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$database = $pools
|
||||
|
||||
$adapter = $pools
|
||||
->get($project->getAttribute('database'))
|
||||
->pop()
|
||||
->getResource()
|
||||
;
|
||||
->getResource();
|
||||
|
||||
$adapter = new Database($database, $cache);
|
||||
$adapter->setNamespace('_' . $project->getInternalId());
|
||||
return $adapter;
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
$database
|
||||
->setMetadata('host', \gethostname())
|
||||
->setMetadata('project', $project->getId());
|
||||
|
||||
if ($project->getAttribute('database') === DATABASE_SHARED_TABLES) {
|
||||
$database
|
||||
->setShareTables(true)
|
||||
->setTenant($project->getInternalId())
|
||||
->setNamespace('');
|
||||
} else {
|
||||
$database
|
||||
->setShareTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getInternalId());
|
||||
}
|
||||
|
||||
return $database;
|
||||
}, ['cache', 'register', 'message', 'dbForConsole']);
|
||||
|
||||
Server::setResource('project', function (Message $message, Database $dbForConsole) {
|
||||
@@ -95,7 +113,19 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForConso
|
||||
|
||||
if (isset($databases[$databaseName])) {
|
||||
$database = $databases[$databaseName];
|
||||
$database->setNamespace('_' . $project->getInternalId());
|
||||
|
||||
if ($project->getAttribute('database') === DATABASE_SHARED_TABLES) {
|
||||
$database
|
||||
->setShareTables(true)
|
||||
->setTenant($project->getInternalId())
|
||||
->setNamespace('');
|
||||
} else {
|
||||
$database
|
||||
->setShareTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getInternalId());
|
||||
}
|
||||
|
||||
return $database;
|
||||
}
|
||||
|
||||
@@ -108,7 +138,17 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForConso
|
||||
|
||||
$databases[$databaseName] = $database;
|
||||
|
||||
$database->setNamespace('_' . $project->getInternalId());
|
||||
if ($project->getAttribute('database') === DATABASE_SHARED_TABLES) {
|
||||
$database
|
||||
->setShareTables(true)
|
||||
->setTenant($project->getInternalId())
|
||||
->setNamespace('');
|
||||
} else {
|
||||
$database
|
||||
->setShareTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getInternalId());
|
||||
}
|
||||
|
||||
return $database;
|
||||
};
|
||||
@@ -145,6 +185,9 @@ Server::setResource('log', fn() => new Log());
|
||||
Server::setResource('queueForUsage', function (Connection $queue) {
|
||||
return new Usage($queue);
|
||||
}, ['queue']);
|
||||
Server::setResource('queueForUsageDump', function (Connection $queue) {
|
||||
return new UsageDump($queue);
|
||||
}, ['queue']);
|
||||
Server::setResource('queue', function (Group $pools) {
|
||||
return $pools->get('queue')->pop()->getResource();
|
||||
}, ['pools']);
|
||||
@@ -178,6 +221,9 @@ Server::setResource('queueForCertificates', function (Connection $queue) {
|
||||
Server::setResource('queueForMigrations', function (Connection $queue) {
|
||||
return new Migration($queue);
|
||||
}, ['queue']);
|
||||
Server::setResource('queueForHamster', function (Connection $queue) {
|
||||
return new Hamster($queue);
|
||||
}, ['queue']);
|
||||
Server::setResource('logger', function (Registry $register) {
|
||||
return $register->get('logger');
|
||||
}, ['register']);
|
||||
@@ -227,7 +273,7 @@ try {
|
||||
* Any worker can be configured with the following env vars:
|
||||
* - _APP_WORKERS_NUM The total number of worker processes
|
||||
* - _APP_WORKER_PER_CORE The number of worker processes per core (ignored if _APP_WORKERS_NUM is set)
|
||||
* - _APP_QUEUE_NAME The name of the queue to read for database events
|
||||
* - _APP_QUEUE_NAME The name of the queue to read for database events
|
||||
*/
|
||||
if ($workerName === 'databases') {
|
||||
$queueName = App::getEnv('_APP_QUEUE_NAME', 'database_db_main');
|
||||
@@ -245,7 +291,6 @@ try {
|
||||
Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine());
|
||||
}
|
||||
|
||||
|
||||
$worker = $platform->getWorker();
|
||||
|
||||
$worker
|
||||
@@ -260,7 +305,8 @@ $worker
|
||||
->inject('error')
|
||||
->inject('logger')
|
||||
->inject('log')
|
||||
->action(function (Throwable $error, ?Logger $logger, Log $log) {
|
||||
->inject('project')
|
||||
->action(function (Throwable $error, ?Logger $logger, Log $log, Document $project) {
|
||||
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
if ($error instanceof PDOException) {
|
||||
@@ -276,6 +322,7 @@ $worker
|
||||
$log->setAction('appwrite-queue-' . App::getEnv('QUEUE'));
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('projectId', $project->getId() ?? 'n/a');
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
@@ -295,12 +342,9 @@ $worker
|
||||
Console::error('[Error] Line: ' . $error->getLine());
|
||||
});
|
||||
|
||||
try {
|
||||
$workerStart = $worker->getWorkerStart();
|
||||
} catch (\Throwable $error) {
|
||||
$worker->workerStart();
|
||||
} finally {
|
||||
Console::info("Worker $workerName started");
|
||||
}
|
||||
$worker->workerStart()
|
||||
->action(function () use ($workerName) {
|
||||
Console::info("Worker $workerName started");
|
||||
});
|
||||
|
||||
$worker->start();
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/cli.php queue-count --type=failed $@
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/cli.php queue-count --type=processing $@
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/cli.php queue-count --type=success $@
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/cli.php queue-retry $@
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/worker.php hamster $@
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/worker.php usage-dump $@
|
||||
+10
-9
@@ -1,4 +1,5 @@
|
||||
{
|
||||
|
||||
"name": "appwrite/server-ce",
|
||||
"description": "End to end backend server for frontend and mobile apps.",
|
||||
"type": "project",
|
||||
@@ -43,26 +44,26 @@
|
||||
"ext-sockets": "*",
|
||||
"appwrite/php-runtimes": "0.13.*",
|
||||
"appwrite/php-clamav": "2.0.*",
|
||||
"utopia-php/abuse": "0.33.*",
|
||||
"utopia-php/abuse": "0.34.*",
|
||||
"utopia-php/analytics": "0.10.*",
|
||||
"utopia-php/audit": "0.35.*",
|
||||
"utopia-php/cache": "0.8.*",
|
||||
"utopia-php/audit": "0.36.*",
|
||||
"utopia-php/cache": "0.9.*",
|
||||
"utopia-php/cli": "0.15.*",
|
||||
"utopia-php/config": "0.2.*",
|
||||
"utopia-php/database": "0.45.*",
|
||||
"utopia-php/domains": "0.3.*",
|
||||
"utopia-php/database": "0.45.7",
|
||||
"utopia-php/domains": "0.5.*",
|
||||
"utopia-php/dsn": "0.1.*",
|
||||
"utopia-php/framework": "0.31.0",
|
||||
"utopia-php/image": "0.5.*",
|
||||
"utopia-php/framework": "0.33.*",
|
||||
"utopia-php/image": "0.6.*",
|
||||
"utopia-php/locale": "0.4.*",
|
||||
"utopia-php/logger": "0.3.*",
|
||||
"utopia-php/messaging": "0.2.*",
|
||||
"utopia-php/messaging": "0.3.*",
|
||||
"utopia-php/migration": "0.3.*",
|
||||
"utopia-php/orchestration": "0.9.*",
|
||||
"utopia-php/platform": "0.5.*",
|
||||
"utopia-php/pools": "0.4.*",
|
||||
"utopia-php/preloader": "0.2.*",
|
||||
"utopia-php/queue": "0.6.*",
|
||||
"utopia-php/queue": "0.7.*",
|
||||
"utopia-php/registry": "0.5.*",
|
||||
"utopia-php/storage": "0.18.*",
|
||||
"utopia-php/swoole": "0.5.*",
|
||||
|
||||
Generated
+229
-230
File diff suppressed because it is too large
Load Diff
+119
-33
@@ -5,12 +5,12 @@
|
||||
|
||||
x-logging: &x-logging
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-file: '5'
|
||||
max-size: '10m'
|
||||
max-file: "5"
|
||||
max-size: "10m"
|
||||
|
||||
version: '3'
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
@@ -53,7 +53,7 @@ services:
|
||||
DEBUG: false
|
||||
TESTING: true
|
||||
VERSION: dev
|
||||
ports:
|
||||
ports:
|
||||
- 9501:80
|
||||
networks:
|
||||
- appwrite
|
||||
@@ -88,7 +88,7 @@ services:
|
||||
- mariadb
|
||||
- redis
|
||||
# - clamav
|
||||
entrypoint:
|
||||
entrypoint:
|
||||
- php
|
||||
- -e
|
||||
- app/http.php
|
||||
@@ -100,6 +100,7 @@ services:
|
||||
- _APP_CONSOLE_WHITELIST_ROOT
|
||||
- _APP_CONSOLE_WHITELIST_EMAILS
|
||||
- _APP_CONSOLE_WHITELIST_IPS
|
||||
- _APP_CONSOLE_HOSTNAMES
|
||||
- _APP_SYSTEM_EMAIL_NAME
|
||||
- _APP_SYSTEM_EMAIL_ADDRESS
|
||||
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
|
||||
@@ -191,7 +192,7 @@ services:
|
||||
container_name: appwrite-realtime
|
||||
image: appwrite-dev
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
ports:
|
||||
- 9505:80
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
@@ -299,7 +300,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
volumes:
|
||||
volumes:
|
||||
- appwrite-uploads:/storage/uploads:rw
|
||||
- appwrite-cache:/storage/cache:rw
|
||||
- appwrite-functions:/storage/functions:rw
|
||||
@@ -353,7 +354,7 @@ services:
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
volumes:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
depends_on:
|
||||
@@ -452,7 +453,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
volumes:
|
||||
volumes:
|
||||
- appwrite-config:/storage/config:rw
|
||||
- appwrite-certificates:/storage/certificates:rw
|
||||
- ./app:/usr/src/code/app
|
||||
@@ -570,6 +571,7 @@ services:
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_SMS_PROVIDER
|
||||
- _APP_SMS_FROM
|
||||
- _APP_SMS_PROJECTS_DENY_LIST
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
@@ -643,6 +645,7 @@ services:
|
||||
- _APP_MAINTENANCE_RETENTION_AUDIT
|
||||
- _APP_MAINTENANCE_RETENTION_USAGE_HOURLY
|
||||
- _APP_MAINTENANCE_RETENTION_SCHEDULES
|
||||
- _APP_MAINTENANCE_DELAY
|
||||
|
||||
appwrite-worker-usage:
|
||||
entrypoint: worker-usage
|
||||
@@ -673,6 +676,38 @@ services:
|
||||
- _APP_USAGE_STATS
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_USAGE_AGGREGATION_INTERVAL
|
||||
|
||||
appwrite-worker-usage-dump:
|
||||
entrypoint: worker-usage-dump
|
||||
<<: *x-logging
|
||||
container_name: appwrite-worker-usage-dump
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_USAGE_STATS
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_USAGE_AGGREGATION_INTERVAL
|
||||
|
||||
appwrite-schedule:
|
||||
entrypoint: schedule
|
||||
@@ -703,12 +738,69 @@ services:
|
||||
|
||||
appwrite-assistant:
|
||||
container_name: appwrite-assistant
|
||||
image: appwrite/assistant:0.2.2
|
||||
image: appwrite/assistant:0.3.0
|
||||
networks:
|
||||
- appwrite
|
||||
environment:
|
||||
- _APP_ASSISTANT_OPENAI_API_KEY
|
||||
|
||||
appwrite-worker-hamster:
|
||||
entrypoint: worker-hamster
|
||||
<<: *x-logging
|
||||
container_name: appwrite-worker-hamster
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_MIXPANEL_TOKEN
|
||||
|
||||
appwrite-hamster-scheduler:
|
||||
entrypoint: hamster
|
||||
<<: *x-logging
|
||||
container_name: appwrite-hamster-scheduler
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_HAMSTER_TIME
|
||||
- _APP_HAMSTER_INTERVAL
|
||||
|
||||
openruntimes-executor:
|
||||
container_name: openruntimes-executor
|
||||
hostname: appwrite-executor
|
||||
@@ -797,7 +889,7 @@ services:
|
||||
- MYSQL_DATABASE=${_APP_DB_SCHEMA}
|
||||
- MYSQL_USER=${_APP_DB_USER}
|
||||
- MYSQL_PASSWORD=${_APP_DB_PASS}
|
||||
command: 'mysqld --innodb-flush-method=fsync' # add ' --query_cache_size=0' for DB tests
|
||||
command: "mysqld --innodb-flush-method=fsync" # add ' --query_cache_size=0' for DB tests
|
||||
# command: mv /var/lib/mysql/ib_logfile0 /var/lib/mysql/ib_logfile0.bu && mv /var/lib/mysql/ib_logfile1 /var/lib/mysql/ib_logfile1.bu
|
||||
|
||||
# smtp:
|
||||
@@ -843,15 +935,16 @@ services:
|
||||
#
|
||||
# MailCatcher - An SMTP server. Catches all system emails and displays them in a nice UI.
|
||||
# RequestCatcher - An HTTP server. Catches all system https calls and displays them using a simple HTTP API. Used to debug & tests webhooks and HTTP tasks
|
||||
# RedisCommander - A nice UI for exploring Redis data
|
||||
# Webgrind - A nice UI for exploring and debugging code-level stuff
|
||||
# Redis Insight - A nice UI for exploring Redis data
|
||||
# Adminer - A nice UI for exploring MariaDB data
|
||||
# GraphQl Explorer - A nice UI for exploring GraphQL API
|
||||
|
||||
maildev: # used mainly for dev tests
|
||||
image: appwrite/mailcatcher:1.0.0
|
||||
container_name: appwrite-mailcatcher
|
||||
<<: *x-logging
|
||||
ports:
|
||||
- '9503:1080'
|
||||
- "9503:1080"
|
||||
networks:
|
||||
- appwrite
|
||||
|
||||
@@ -860,7 +953,7 @@ services:
|
||||
container_name: appwrite-requestcatcher
|
||||
<<: *x-logging
|
||||
ports:
|
||||
- '9504:5000'
|
||||
- "9504:5000"
|
||||
networks:
|
||||
- appwrite
|
||||
|
||||
@@ -874,22 +967,16 @@ services:
|
||||
networks:
|
||||
- appwrite
|
||||
|
||||
# redis-commander:
|
||||
# image: rediscommander/redis-commander:latest
|
||||
# restart: unless-stopped
|
||||
# networks:
|
||||
# - appwrite
|
||||
# environment:
|
||||
# - REDIS_HOSTS=redis
|
||||
# ports:
|
||||
# - "8081:8081"
|
||||
# webgrind:
|
||||
# image: 'jokkedk/webgrind:latest'
|
||||
# volumes:
|
||||
# - './debug:/tmp'
|
||||
# ports:
|
||||
# - '3001:80'
|
||||
|
||||
redis-insight:
|
||||
image: redis/redisinsight:latest
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
environment:
|
||||
- REDIS_HOSTS=redis
|
||||
ports:
|
||||
- "8081:5540"
|
||||
|
||||
graphql-explorer:
|
||||
container_name: appwrite-graphql-explorer
|
||||
image: appwrite/altair:0.3.0
|
||||
@@ -901,7 +988,6 @@ services:
|
||||
environment:
|
||||
- SERVER_URL=http://localhost/v1/graphql
|
||||
|
||||
|
||||
# Dev Tools End ------------------------------------------------------------------------------------------
|
||||
|
||||
networks:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.create_phone_verification()
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.create_recovery(email: 'email@example.com', url: 'https://example.com')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.create_verification(url: 'https://example.com')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.delete_identity(identity_id: '[IDENTITY_ID]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.delete_session(session_id: '[SESSION_ID]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.delete_sessions()
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.get_prefs()
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.get_session(session_id: '[SESSION_ID]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.get()
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.list_identities()
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.list_logs()
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.list_sessions()
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.update_email(email: 'email@example.com', password: 'password')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.update_name(name: '[NAME]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.update_password(password: '')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.update_phone_verification(user_id: '[USER_ID]', secret: '[SECRET]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.update_phone(phone: '+12065550100', password: 'password')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.update_prefs(prefs: {})
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.update_recovery(user_id: '[USER_ID]', secret: '[SECRET]', password: 'password', password_again: 'password')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.update_session(session_id: '[SESSION_ID]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.update_status()
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.update_verification(user_id: '[USER_ID]', secret: '[SECRET]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ avatars = Avatars.new(client)
|
||||
|
||||
response = avatars.get_browser(code: 'aa')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ avatars = Avatars.new(client)
|
||||
|
||||
response = avatars.get_credit_card(code: 'amex')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ avatars = Avatars.new(client)
|
||||
|
||||
response = avatars.get_favicon(url: 'https://example.com')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ avatars = Avatars.new(client)
|
||||
|
||||
response = avatars.get_flag(code: 'af')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ avatars = Avatars.new(client)
|
||||
|
||||
response = avatars.get_image(url: 'https://example.com')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ avatars = Avatars.new(client)
|
||||
|
||||
response = avatars.get_initials()
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ avatars = Avatars.new(client)
|
||||
|
||||
response = avatars.get_qr(text: '[TEXT]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_boolean_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '', required: false)
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_collection(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', name: '[NAME]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_datetime_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '', required: false)
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_document(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', document_id: '[DOCUMENT_ID]', data: {})
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_email_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '', required: false)
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_enum_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '', elements: [], required: false)
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_float_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '', required: false)
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_index(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '', type: 'key', attributes: [])
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_integer_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '', required: false)
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_ip_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '', required: false)
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_relationship_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', related_collection_id: '[RELATED_COLLECTION_ID]', type: 'oneToOne')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_string_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '', size: 1, required: false)
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_url_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '', required: false)
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create(database_id: '[DATABASE_ID]', name: '[NAME]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.delete_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.delete_collection(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.delete_document(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', document_id: '[DOCUMENT_ID]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.delete_index(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user