mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
499
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebc3febc38 | ||
|
|
8fcba8b5c9 | ||
|
|
6c8b34c230 | ||
|
|
377ade3c44 | ||
|
|
71533aaaf3 | ||
|
|
ccbe7e5d3f | ||
|
|
bea7739d7e | ||
|
|
76320d652a | ||
|
|
6d5968e2ea | ||
|
|
70c380fa36 | ||
|
|
512a7ae2bd | ||
|
|
49d3cd980f | ||
|
|
39a396810a | ||
|
|
1d307178bb | ||
|
|
c3ed2ff6ce | ||
|
|
efadf17bfe | ||
|
|
bcfec8d5de | ||
|
|
4b2e22d9da | ||
|
|
45e22e2243 | ||
|
|
0a21f3b139 | ||
|
|
cc8eb62c83 | ||
|
|
b1ad6ea87c | ||
|
|
d52d6c0bf0 | ||
|
|
db406b0a27 | ||
|
|
2585518e33 | ||
|
|
c9fceb870c | ||
|
|
9f1ec356d1 | ||
|
|
28d285d5c5 | ||
|
|
a941d8b855 | ||
|
|
3263133e5f | ||
|
|
a3ba66c90a | ||
|
|
c24d724000 | ||
|
|
df5ccc10ad | ||
|
|
2807d6cd9a | ||
|
|
20e2f2284f | ||
|
|
7a0d69c826 | ||
|
|
feedec80f2 | ||
|
|
f4d40a1289 | ||
|
|
1e65f075e6 | ||
|
|
68725d9262 | ||
|
|
5b805d686b | ||
|
|
584acafb1d | ||
|
|
dce7856b77 | ||
|
|
a6af609317 | ||
|
|
035f6244e1 | ||
|
|
723cb1a488 | ||
|
|
815209ebb0 | ||
|
|
53c74582fc | ||
|
|
78bbe77580 | ||
|
|
0c3871a681 | ||
|
|
b59eba4ec6 | ||
|
|
cb4c97f2ee | ||
|
|
2ee2ea09a0 | ||
|
|
4ec84a147f | ||
|
|
e583de4650 | ||
|
|
98af2a5eb3 | ||
|
|
e3ad0f85de | ||
|
|
a26382ac51 | ||
|
|
9ba182d8a0 | ||
|
|
27fc8058b9 | ||
|
|
c5bd8c712f | ||
|
|
a1267b1bff | ||
|
|
18d17ea945 | ||
|
|
fabd9559c4 | ||
|
|
ec637d4417 | ||
|
|
4523e86b91 | ||
|
|
1ea108c2ce | ||
|
|
0a864e51b8 | ||
|
|
0cce480592 | ||
|
|
b622b092a8 | ||
|
|
96fe989f6d | ||
|
|
c6dd7de216 | ||
|
|
f77a64bff9 | ||
|
|
d13dbae0fe | ||
|
|
f552a1ba15 | ||
|
|
7282c5d51f | ||
|
|
82ec75d582 | ||
|
|
6bf6142667 | ||
|
|
938e65cb02 | ||
|
|
114de91f48 | ||
|
|
2e6f3f5c14 | ||
|
|
7b3d9bae03 | ||
|
|
d81a1154e3 | ||
|
|
a944c65660 | ||
|
|
856046dc82 | ||
|
|
2ca551123d | ||
|
|
c861d45749 | ||
|
|
4a43969da9 | ||
|
|
6fa4122910 | ||
|
|
1d27101770 | ||
|
|
9214decc8d | ||
|
|
ee4ae3bd47 | ||
|
|
53a114e7e4 | ||
|
|
f2df9cb93a | ||
|
|
d69726487e | ||
|
|
5fccb8cc28 | ||
|
|
f371237fd5 | ||
|
|
21a0d60c98 | ||
|
|
4eb8534294 | ||
|
|
c95f905bce | ||
|
|
0293da1e22 | ||
|
|
a4a0c4175d | ||
|
|
9548d18a3e | ||
|
|
d3c73fbb49 | ||
|
|
7928175387 | ||
|
|
8818187740 | ||
|
|
75324b24fc | ||
|
|
d6d118f4ab | ||
|
|
e998739998 | ||
|
|
dd29967e99 | ||
|
|
e7f5ae9306 | ||
|
|
4133ec99ae | ||
|
|
6dd63ee152 | ||
|
|
912dbda159 | ||
|
|
410a050244 | ||
|
|
9cf45816c2 | ||
|
|
386fc995e6 | ||
|
|
d6451b8fad | ||
|
|
920ddd18e6 | ||
|
|
ff9334ab78 | ||
|
|
c494e96f79 | ||
|
|
fd78f0f7df | ||
|
|
4ff10dcacc | ||
|
|
7a995fe759 | ||
|
|
227606baef | ||
|
|
f315e759f3 | ||
|
|
8a6fba75a5 | ||
|
|
196c76ac70 | ||
|
|
7f82484436 | ||
|
|
db18caf739 | ||
|
|
6693e28c93 | ||
|
|
bf489ce13b | ||
|
|
357d6482f9 | ||
|
|
eec72a915a | ||
|
|
ae29e4c424 | ||
|
|
05dc264df9 | ||
|
|
597b20a6cb | ||
|
|
6b7d9853f9 | ||
|
|
20f80ac067 | ||
|
|
8bfa659120 | ||
|
|
65e44d76a1 | ||
|
|
bb07808661 | ||
|
|
d18550ff54 | ||
|
|
84dc921d41 | ||
|
|
e2d7dd837d | ||
|
|
9feb204fd5 | ||
|
|
3880f181b3 | ||
|
|
44a37e9e20 | ||
|
|
e6cfedd340 | ||
|
|
e4d1178e71 | ||
|
|
929bdcef25 | ||
|
|
3f725c6be9 | ||
|
|
990a32dd9e | ||
|
|
c5b8ed9cc1 | ||
|
|
fec573d23b | ||
|
|
a144968d70 | ||
|
|
6fa0724404 | ||
|
|
b1ce71e6b0 | ||
|
|
939092726c | ||
|
|
c6f8599c75 | ||
|
|
91d8519940 | ||
|
|
a90f79f1c1 | ||
|
|
f880b6e8c3 | ||
|
|
b8d65326e6 | ||
|
|
a9fd82e406 | ||
|
|
a8c2491fbb | ||
|
|
7f6486ec80 | ||
|
|
388cec1737 | ||
|
|
2f5a49a37d | ||
|
|
1f93184c42 | ||
|
|
7d9cf48ca3 | ||
|
|
eef2a7abdf | ||
|
|
c7a022ba43 | ||
|
|
96a84a8fd7 | ||
|
|
cea242c66f | ||
|
|
ce4eb563b3 | ||
|
|
2307d637fb | ||
|
|
0aa72aafae | ||
|
|
6e1f0d57af | ||
|
|
62b6ef06e6 | ||
|
|
f5ab593261 | ||
|
|
dd4a43b78c | ||
|
|
c4c56283e5 | ||
|
|
f71a98a527 | ||
|
|
2258668769 | ||
|
|
6dba407aed | ||
|
|
7eef845556 | ||
|
|
7781d377ae | ||
|
|
59bf4663ca | ||
|
|
6653dbbb92 | ||
|
|
d7d20ccb29 | ||
|
|
2d54986b09 | ||
|
|
9ea0b2bc2f | ||
|
|
715727853b | ||
|
|
43d4f709d5 | ||
|
|
f40050fe6f | ||
|
|
308a534d98 | ||
|
|
35a72c4f08 | ||
|
|
34dfcba45c | ||
|
|
cb1e31bcd4 | ||
|
|
3e4e7fc0cd | ||
|
|
da2444ede2 | ||
|
|
bc224de751 | ||
|
|
9403e4d65d | ||
|
|
07a49835a6 | ||
|
|
d32de6f217 | ||
|
|
9b00ce4f1d | ||
|
|
ca62504b5a | ||
|
|
8f6c8f9d8d | ||
|
|
23fcb284a1 | ||
|
|
ffdd0815a7 | ||
|
|
4260324153 | ||
|
|
d66813d3cf | ||
|
|
0ce92da41f | ||
|
|
e8ef4e40d7 | ||
|
|
d200b07466 | ||
|
|
92abfb31aa | ||
|
|
6c56eee0f4 | ||
|
|
399c37d943 | ||
|
|
f3acadd53c | ||
|
|
331fcee710 | ||
|
|
951a96ae01 | ||
|
|
ed1680837d | ||
|
|
7864a5b9d1 | ||
|
|
f11bd7ce0e | ||
|
|
fffc6795a7 | ||
|
|
ed91549681 | ||
|
|
04173600ae | ||
|
|
d5fe5c34af | ||
|
|
6bc9adece8 | ||
|
|
97d46c6273 | ||
|
|
9d78a8e6b6 | ||
|
|
d12a6f5168 | ||
|
|
0f47e6ea28 | ||
|
|
77ceb15d7d | ||
|
|
592629587d | ||
|
|
187fde4a4e | ||
|
|
b74d4d45f9 | ||
|
|
aa9a58b44d | ||
|
|
59a773e9a0 | ||
|
|
e3053bb83d | ||
|
|
1f7fc4bd40 | ||
|
|
d1b59ff3f3 | ||
|
|
221b52bac0 | ||
|
|
b8ed30db55 | ||
|
|
be56317bf2 | ||
|
|
31728c9b72 | ||
|
|
9d0fc9e5b1 | ||
|
|
2dce141d17 | ||
|
|
d421b5ff30 | ||
|
|
b8eb0810c2 | ||
|
|
cb74a5756a | ||
|
|
d1549b225b | ||
|
|
ba25849871 | ||
|
|
44f3bbae03 | ||
|
|
2cd91f509b | ||
|
|
a06ba57384 | ||
|
|
9ac686ae31 | ||
|
|
7385bc1d9f | ||
|
|
dffcfe8ee4 | ||
|
|
e3f373b8d2 | ||
|
|
8b591f3522 | ||
|
|
452440f3c0 | ||
|
|
4e10cabec0 | ||
|
|
5d1da00138 | ||
|
|
b236e2546b | ||
|
|
412d09b801 | ||
|
|
5ab28ad99a | ||
|
|
9be447aacf | ||
|
|
66e68aea14 | ||
|
|
a332eb5f32 | ||
|
|
775d21e5ee | ||
|
|
f6484294da | ||
|
|
f3b9121a14 | ||
|
|
cf8519bfa5 | ||
|
|
860cf8cd57 | ||
|
|
c978b6f34f | ||
|
|
f3f2855fe5 | ||
|
|
3cb53f0604 | ||
|
|
a5f45b46e9 | ||
|
|
2c1813198d | ||
|
|
130c2221ec | ||
|
|
912ea37af6 | ||
|
|
ccc74dea74 | ||
|
|
c08e5f706c | ||
|
|
a6c6a5624a | ||
|
|
ee5bb6d73d | ||
|
|
bfbf180aee | ||
|
|
d8a3b53641 | ||
|
|
3018b478ba | ||
|
|
df4dbcf607 | ||
|
|
094fe90499 | ||
|
|
58a90cca4a | ||
|
|
29b0ebb3bd | ||
|
|
f0ccd1f586 | ||
|
|
90e705f8c5 | ||
|
|
17076e4a00 | ||
|
|
7c50bbc500 | ||
|
|
4df5f4a18f | ||
|
|
e8bcc94187 | ||
|
|
a071c715bc | ||
|
|
52981e0164 | ||
|
|
e576cd5082 | ||
|
|
387668bb47 | ||
|
|
30befc6a60 | ||
|
|
d13644a47a | ||
|
|
4a905a6ac9 | ||
|
|
b6e95f4502 | ||
|
|
1900492aad | ||
|
|
cb9d3869f0 | ||
|
|
96f4ef7d22 | ||
|
|
8e02a5a227 | ||
|
|
d17ae517d3 | ||
|
|
c2dd8aceda | ||
|
|
24fcf4247c | ||
|
|
2dc20ef0eb | ||
|
|
04943b6313 | ||
|
|
eb366cf94b | ||
|
|
77b4f8b7a0 | ||
|
|
24bd4c3d7b | ||
|
|
25d9891f2b | ||
|
|
a928fcd40f | ||
|
|
2d34301834 | ||
|
|
cf7259e060 | ||
|
|
8c9f9c6f58 | ||
|
|
33f8e35b62 | ||
|
|
2c1060f57a | ||
|
|
30e0ca81bd | ||
|
|
92cc382a6b | ||
|
|
c322cc3ffe | ||
|
|
7d428ffe83 | ||
|
|
fb96aecbef | ||
|
|
80d5d4716a | ||
|
|
c1dde09070 | ||
|
|
cf99269bc5 | ||
|
|
2a184288f4 | ||
|
|
9237f387fc | ||
|
|
d531c29fc8 | ||
|
|
b554243447 | ||
|
|
7044601603 | ||
|
|
53bc126015 | ||
|
|
f1dc468e50 | ||
|
|
15b2ab321e | ||
|
|
ce63b00cf4 | ||
|
|
b9aaecba25 | ||
|
|
789870b545 | ||
|
|
c9f7b7f0d9 | ||
|
|
cba7e53898 | ||
|
|
fb26da5df1 | ||
|
|
d9c606a1c2 | ||
|
|
eb8455bd76 | ||
|
|
a76a03d988 | ||
|
|
d9eb69aa47 | ||
|
|
e250b413f0 | ||
|
|
8ccfb1aebb | ||
|
|
9da4f19d4f | ||
|
|
cc04c682b0 | ||
|
|
66ba483b6a | ||
|
|
908e408480 | ||
|
|
7f9ce1ca85 | ||
|
|
168166b9c1 | ||
|
|
e42a26fdad | ||
|
|
2c4770d29b | ||
|
|
2a9e423cb1 | ||
|
|
14869cc6d6 | ||
|
|
fe99470374 | ||
|
|
ff903e7cbb | ||
|
|
10ebf942b1 | ||
|
|
d9a75c5938 | ||
|
|
bb80e50d01 | ||
|
|
644840ec66 | ||
|
|
71e714c66b | ||
|
|
b2b187c57d | ||
|
|
ecc76c7520 | ||
|
|
898e8e214b | ||
|
|
43db2ddf6e | ||
|
|
7371b68418 | ||
|
|
7f4d5f692d | ||
|
|
8113854a89 | ||
|
|
eb097a037b | ||
|
|
cbfdd27834 | ||
|
|
113c266881 | ||
|
|
614db7388e | ||
|
|
cbc5259db3 | ||
|
|
0d6d6a0a35 | ||
|
|
e8d4065af5 | ||
|
|
008110b3f7 | ||
|
|
efb0cae4e0 | ||
|
|
ae99d59aba | ||
|
|
a06aaaf9ca | ||
|
|
4970d1b6f2 | ||
|
|
094cd180b5 | ||
|
|
a8f43f3486 | ||
|
|
038f4b5992 | ||
|
|
c903fb87ac | ||
|
|
e5841c4cc0 | ||
|
|
bb7815bd57 | ||
|
|
0c33d981a7 | ||
|
|
36d8ad6e7c | ||
|
|
fbce66d500 | ||
|
|
89db65299d | ||
|
|
ea6a05be4f | ||
|
|
355acf6824 | ||
|
|
c2988efa08 | ||
|
|
8ccf5094f8 | ||
|
|
39f2d24907 | ||
|
|
d3c5a425e7 | ||
|
|
85703d29e1 | ||
|
|
d3c11b5c93 | ||
|
|
6f177a0a7a | ||
|
|
dc48bb35ef | ||
|
|
8218f36d34 | ||
|
|
0fe906c538 | ||
|
|
4641596a6d | ||
|
|
d932527561 | ||
|
|
89c072e223 | ||
|
|
6421bc8689 | ||
|
|
d008d9bff0 | ||
|
|
f6dc6359bb | ||
|
|
31410879a7 | ||
|
|
343e352b17 | ||
|
|
b38ea72407 | ||
|
|
34f23bdc9f | ||
|
|
032638e896 | ||
|
|
10cc6a8040 | ||
|
|
9ecdbf5950 | ||
|
|
55b436c67b | ||
|
|
f74a60dba4 | ||
|
|
05defcc6e0 | ||
|
|
6700340ef3 | ||
|
|
4224f6ea5a | ||
|
|
8d925f3670 | ||
|
|
d2875c9bf6 | ||
|
|
c002f4afe3 | ||
|
|
ecb8104340 | ||
|
|
e6090800e2 | ||
|
|
92b7760e94 | ||
|
|
fe988f4489 | ||
|
|
0237cced6b | ||
|
|
625ec4ce91 | ||
|
|
5339592b3f | ||
|
|
9f1d092717 | ||
|
|
e38a4e4347 | ||
|
|
fdbc5b6737 | ||
|
|
fa2b7955d0 | ||
|
|
8b3d3c6f8b | ||
|
|
ea11af1e15 | ||
|
|
6cbae88131 | ||
|
|
03b2755aaf | ||
|
|
2095a94bc5 | ||
|
|
df7796d5cb | ||
|
|
d177e3adfc | ||
|
|
d04ac5c3e6 | ||
|
|
60939da801 | ||
|
|
cdb301a293 | ||
|
|
14a9aa890f | ||
|
|
d3833d06d5 | ||
|
|
5ba19ec763 | ||
|
|
0564936c4f | ||
|
|
c2795376d8 | ||
|
|
2b898dfe50 | ||
|
|
fa1404be52 | ||
|
|
be87675e1c | ||
|
|
b1edf75c4e | ||
|
|
8a0f092342 | ||
|
|
87b02de612 | ||
|
|
f8e9f71de3 | ||
|
|
c055c32371 | ||
|
|
d60858cd8f | ||
|
|
22bd655ad1 | ||
|
|
c900b22dc0 | ||
|
|
27e5ade92b | ||
|
|
d9c1b9db2a | ||
|
|
eb399c7bd0 | ||
|
|
b8e51366d7 | ||
|
|
b75cd993be | ||
|
|
a1bc503ce4 | ||
|
|
e475a7ac5a | ||
|
|
2dc24dabfd | ||
|
|
aaa2a0525f | ||
|
|
5e04480cc0 | ||
|
|
ea2b1519d5 | ||
|
|
e5385f7512 | ||
|
|
d8bf4b9f89 | ||
|
|
2d2c55e07c | ||
|
|
da6dff2d17 | ||
|
|
b4aab6134e | ||
|
|
1c465a8cda | ||
|
|
8ab6c2d83b | ||
|
|
c89d075200 | ||
|
|
6c6c71484d | ||
|
|
18a66bae4e | ||
|
|
68b72f3162 | ||
|
|
f6438065e3 | ||
|
|
4c6961f7d6 | ||
|
|
0bd27c4182 | ||
|
|
ddcfac4f0f | ||
|
|
3e58014dbd | ||
|
|
ff06920e24 |
@@ -0,0 +1,29 @@
|
||||
# Patch Release Checklist for Appwrite
|
||||
|
||||
When bumping a patch version (e.g., `1.9.0` -> `1.9.1`), follow this checklist.
|
||||
|
||||
## Checklist
|
||||
|
||||
### Bump console image
|
||||
|
||||
Update the console Docker image tag in both files:
|
||||
- [ ] `docker-compose.yml` -- update `image: appwrite/console:X.Y.Z`
|
||||
- [ ] `app/views/install/compose.phtml` -- update `image: <?php echo $organization; ?>/console:X.Y.Z`
|
||||
|
||||
### Bump Appwrite version
|
||||
|
||||
- [ ] **`app/init/constants.php`** -- update `APP_VERSION_STABLE` to the new version (e.g., `'1.9.1'`). In same file, increment `APP_CACHE_BUSTER` by 1.
|
||||
- [ ] **`README.md`** -- update the Docker image tag `appwrite/appwrite:X.Y.Z` in all 3 install code blocks (Unix, Windows CMD, PowerShell).
|
||||
- [ ] **`README-CN.md`** -- same Docker image tag update in all 3 install code blocks.
|
||||
- [ ] **`src/Appwrite/Migration/Migration.php`** -- add the new version to the `$versions` array, mapping it to a migration class. If new class exists, use that, otherwise use sle same class as previous version
|
||||
|
||||
### Update CHANGES.md
|
||||
|
||||
- [ ] Add a new `# Version X.Y.Z` section at the top of `CHANGES.md` with subsections: `### Notable changes`, `### Fixes`, `### Miscellaneous`
|
||||
|
||||
## Final review
|
||||
|
||||
- [ ] Ask user to review changes before commiting
|
||||
- [ ] Ask user to update `CHANGES.md` with PRs
|
||||
- [ ] Ask user to generate specs, if needed
|
||||
- [ ] Ask user to add request and response filters, if needed
|
||||
@@ -161,6 +161,27 @@ jobs:
|
||||
- name: Run PHPStan
|
||||
run: composer analyze -- --no-progress
|
||||
|
||||
specs:
|
||||
name: Checks / Specs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
extensions: swoole
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
|
||||
|
||||
- name: Generate specs
|
||||
run: _APP_STORAGE_LIMIT=5368709120 php app/cli.php specs --version=latest --git=no
|
||||
|
||||
locale:
|
||||
name: Checks / Locale
|
||||
runs-on: ubuntu-latest
|
||||
@@ -459,6 +480,10 @@ jobs:
|
||||
_APP_BROWSER_HOST: http://invalid-browser/v1
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
@@ -487,7 +512,7 @@ jobs:
|
||||
# Services that rely on sequential test method execution (shared static state)
|
||||
FUNCTIONAL_FLAG="--functional"
|
||||
case "${{ matrix.service }}" in
|
||||
Databases|TablesDB|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
|
||||
Databases|TablesDB|Functions|Realtime|GraphQL) FUNCTIONAL_FLAG="" ;;
|
||||
esac
|
||||
|
||||
docker compose exec -T \
|
||||
@@ -533,6 +558,10 @@ jobs:
|
||||
_APP_OPTIONS_ABUSE: enabled
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
@@ -590,6 +619,10 @@ jobs:
|
||||
env:
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
|
||||
@@ -115,6 +115,10 @@ Common injections: `$response`, `$request`, `$dbForProject`, `$dbForPlatform`, `
|
||||
- Never hardcode credentials -- use environment variables.
|
||||
- Code changes may require container restart. No central log location -- check relevant containers.
|
||||
|
||||
## Patch release process
|
||||
|
||||
For bumping patch versions (e.g., `1.9.0` -> `1.9.1`), follow the checklist in `.claude/skills/patch-release-checklist/SKILL.md`. It covers the 4 files that must be updated, console image bumps, CHANGES.md updates, and common pitfalls to avoid.
|
||||
|
||||
## Cross-repo context
|
||||
|
||||
Appwrite is the base server for `appwrite/cloud`. Changes to the Action pattern, module structure, DI system, or response models affect cloud. The `feat-dedicated-db` feature spans cloud, edge, and console.
|
||||
|
||||
+3
-3
@@ -72,7 +72,7 @@ docker run -it --rm \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
|
||||
--entrypoint="install" \
|
||||
appwrite/appwrite:1.9.0
|
||||
appwrite/appwrite:1.9.1
|
||||
```
|
||||
|
||||
### Windows
|
||||
@@ -84,7 +84,7 @@ docker run -it --rm ^
|
||||
--volume //var/run/docker.sock:/var/run/docker.sock ^
|
||||
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
|
||||
--entrypoint="install" ^
|
||||
appwrite/appwrite:1.9.0
|
||||
appwrite/appwrite:1.9.1
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
@@ -94,7 +94,7 @@ docker run -it --rm `
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock `
|
||||
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
|
||||
--entrypoint="install" `
|
||||
appwrite/appwrite:1.9.0
|
||||
appwrite/appwrite:1.9.1
|
||||
```
|
||||
|
||||
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
|
||||
|
||||
@@ -1,43 +1,28 @@
|
||||
> We just announced DB operators for Appwrite Databases - [Learn more](https://appwrite.io/blog/post/announcing-db-operators)
|
||||
|
||||
> Appwrite Cloud is now Generally Available - [Learn more](https://appwrite.io/cloud-ga)
|
||||
|
||||
> [Get started with Appwrite](https://apwr.dev/appcloud)
|
||||
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/55a81268-4ecc-46cd-bdf5-73f7e8662fee" />
|
||||
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://appwrite.io" target="_blank"><img src="./public/images/banner.png" alt="Appwrite banner, with logo and text saying "The Developer's Cloud"></a>
|
||||
<br />
|
||||
<br />
|
||||
<b>Appwrite is a best-in-class, developer-first platform that gives builders everything they need to create scalable, stable, and production-ready software, fast.</b>
|
||||
<h1>Appwrite</h1>
|
||||
<b>Appwrite is an open-source, all-in-one development platform. Use built-in backend infrastructure and web hosting, all from a single place.</b>
|
||||
<br />
|
||||
<br />
|
||||
</p>
|
||||
|
||||
<!-- [](https://travis-ci.com/appwrite/appwrite) -->
|
||||
|
||||
[](https://appwrite.io/company/careers)
|
||||
[](https://hacktoberfest.appwrite.io)
|
||||
[](https://appwrite.io/discord?r=Github)
|
||||
[](https://github.com/appwrite/appwrite/actions)
|
||||
[](https://twitter.com/appwrite)
|
||||
|
||||
<!-- [](https://hub.docker.com/r/appwrite/appwrite) -->
|
||||
<!-- [](docs/tutorials/add-translations.md) -->
|
||||
<!-- [](https://store.appwrite.io) -->
|
||||
[](https://appwrite.io/discord)
|
||||
[](https://x.com/appwrite)
|
||||
[](https://cloud.appwrite.io)
|
||||
|
||||
English | [简体中文](README-CN.md)
|
||||
|
||||
Appwrite is an end-to-end platform for building Web, Mobile, Native, or Backend apps, packaged as a set of Docker microservices. It includes both a backend server and a fully integrated hosting solution for deploying static and server-side rendered frontends. Appwrite abstracts the complexity and repetitiveness required to build modern apps from scratch and allows you to build secure, full-stack applications faster.
|
||||
Appwrite is an open-source development platform for building web, mobile, and AI applications. It brings together backend infrastructure and web hosting in one place, so teams can build, ship, and scale without stitching together a fragmented stack. Appwrite is available as a managed cloud platform and can also be self-hosted on infrastructure you control.
|
||||
|
||||
Using Appwrite, you can easily integrate your app with user authentication and multiple sign-in methods, a database for storing and querying users and team data, storage and file management, image manipulation, Cloud Functions, messaging, and [more services](https://appwrite.io/docs).
|
||||
With Appwrite, you can add authentication, databases, storage, functions, messaging, realtime capabilities, and integrated web app hosting through Sites. It is designed to reduce the repetitive backend work required to launch modern products while giving developers secure primitives and flexible APIs to build production-ready applications faster.
|
||||
|
||||

|
||||
|
||||
Find out more at: [https://appwrite.io](https://appwrite.io).
|
||||
Find out more at [https://appwrite.io](https://appwrite.io).
|
||||
|
||||
Table of Contents:
|
||||
|
||||
- [Products](#products)
|
||||
- [Installation \& Setup](#installation--setup)
|
||||
- [Self-Hosting](#self-hosting)
|
||||
- [Unix](#unix)
|
||||
@@ -47,17 +32,31 @@ Table of Contents:
|
||||
- [Upgrade from an Older Version](#upgrade-from-an-older-version)
|
||||
- [One-Click Setups](#one-click-setups)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Products](#products)
|
||||
- [SDKs](#sdks)
|
||||
- [Client](#client)
|
||||
- [Server](#server)
|
||||
- [Community](#community)
|
||||
- [Architecture](#architecture)
|
||||
- [Contributing](#contributing)
|
||||
- [Security](#security)
|
||||
- [Follow Us](#follow-us)
|
||||
- [License](#license)
|
||||
|
||||
|
||||
## Products
|
||||
|
||||
- **[Appwrite Auth](https://appwrite.io/docs/products/auth)** - Secure user authentication with multiple login methods including email/password, SMS, OAuth, anonymous sessions, and magic links. Includes session management, multi-factor authentication, and user verification flows.
|
||||
|
||||
- **[Appwrite Databases](https://appwrite.io/docs/products/databases)** - Scalable structured data storage with support for databases, tables, and rows. Includes querying, pagination, indexing, and relationships to model complex application data.
|
||||
|
||||
- **[Appwrite Storage](https://appwrite.io/docs/products/storage)** - Secure file storage with support for uploads, downloads, encryption, compression, and file transformations for media and assets.
|
||||
|
||||
- **[Appwrite Functions](https://appwrite.io/docs/products/functions)** - Serverless compute platform to run custom backend logic in isolated runtimes, triggered by events or scheduled jobs.15 runtimes supported.
|
||||
|
||||
- **[Appwrite Messaging](https://appwrite.io/docs/products/messaging)** - Multi-channel messaging system for sending emails, SMS, and push notifications to users for engagement, alerts, and transactional workflows.
|
||||
|
||||
- **[Appwrite Sites](https://appwrite.io/docs/products/sites)** - Integrated hosting platform to deploy and scale web applications with support for custom domains, SSR, and seamless backend integration. Git integration and previews are supported.
|
||||
|
||||
|
||||
## Installation & Setup
|
||||
|
||||
The easiest way to get started with Appwrite is by [signing up for Appwrite Cloud](https://cloud.appwrite.io/). While Appwrite Cloud is in public beta, you can build with Appwrite completely free, and we won't collect your credit card information.
|
||||
@@ -76,7 +75,7 @@ docker run -it --rm \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
|
||||
--entrypoint="install" \
|
||||
appwrite/appwrite:1.9.0
|
||||
appwrite/appwrite:1.9.1
|
||||
```
|
||||
|
||||
### Windows
|
||||
@@ -89,7 +88,7 @@ docker run -it --rm ^
|
||||
--volume //var/run/docker.sock:/var/run/docker.sock ^
|
||||
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
|
||||
--entrypoint="install" ^
|
||||
appwrite/appwrite:1.9.0
|
||||
appwrite/appwrite:1.9.1
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
@@ -100,7 +99,7 @@ docker run -it --rm `
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock `
|
||||
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
|
||||
--entrypoint="install" `
|
||||
appwrite/appwrite:1.9.0
|
||||
appwrite/appwrite:1.9.1
|
||||
```
|
||||
|
||||
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.
|
||||
@@ -168,51 +167,29 @@ Getting started with Appwrite is as easy as creating a new project, choosing you
|
||||
| | [Quick start for Kotlin](https://appwrite.io/docs/quick-starts/kotlin) |
|
||||
| | [Quick start for Swift](https://appwrite.io/docs/quick-starts/swift) |
|
||||
|
||||
### Products
|
||||
|
||||
- [**Account**](https://appwrite.io/docs/references/cloud/client-web/account) - Manage current user authentication and account. Track and manage the user sessions, devices, sign-in methods, and security logs.
|
||||
- [**Users**](https://appwrite.io/docs/server/users) - Manage and list all project users when building backend integrations with Server SDKs.
|
||||
- [**Teams**](https://appwrite.io/docs/references/cloud/client-web/teams) - Manage and group users in teams. Manage memberships, invites, and user roles within a team.
|
||||
- [**Databases**](https://appwrite.io/docs/references/cloud/client-web/databases) - Manage databases, collections, and documents. Read, create, update, and delete documents and filter lists of document collections using advanced filters.
|
||||
- [**Storage**](https://appwrite.io/docs/references/cloud/client-web/storage) - Manage storage files. Read, create, delete, and preview files. Manipulate the preview of your files to perfectly fit your app. All files are scanned by ClamAV and stored in a secure and encrypted way.
|
||||
- [**Functions**](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Customize your Appwrite project by executing your custom code in a secure, isolated environment. You can trigger your code on any Appwrite system event either manually or using a CRON schedule.
|
||||
- [**Messaging**](https://appwrite.io/docs/references/cloud/client-web/messaging) - Communicate with your users through push notifications, emails, and SMS text messages using Appwrite Messaging.
|
||||
- [**Realtime**](https://appwrite.io/docs/realtime) - Listen to real-time events for any of your Appwrite services including users, storage, functions, databases, and more.
|
||||
- [**Locale**](https://appwrite.io/docs/references/cloud/client-web/locale) - Track your user's location and manage your app locale-based data.
|
||||
- [**Avatars**](https://appwrite.io/docs/references/cloud/client-web/avatars) - Manage your users' avatars, countries' flags, browser icons, and credit card symbols. Generate QR codes from links or plaintext strings.
|
||||
- [**MCP**](https://appwrite.io/docs/tooling/mcp) - Use Appwrite's Model Context Protocol (MCP) server to allow LLMs and AI tools like Claude Desktop, Cursor, and Windsurf Editor to directly interact with your Appwrite project through natural language.
|
||||
- [**Sites**](https://appwrite.io/docs/products/sites) - Develop, deploy, and scale your web applications directly from Appwrite, alongside your backend.
|
||||
|
||||
For the complete API documentation, visit [https://appwrite.io/docs](https://appwrite.io/docs). For more tutorials, news and announcements check out our [blog](https://medium.com/appwrite-io) and [Discord Server](https://discord.gg/GSeTUeA).
|
||||
|
||||
### SDKs
|
||||
|
||||
Below is a list of currently supported platforms and languages. If you would like to help us add support to your platform of choice, you can go over to our [SDK Generator](https://github.com/appwrite/sdk-generator) project and view our [contribution guide](https://github.com/appwrite/sdk-generator/blob/master/CONTRIBUTING.md).
|
||||
|
||||
#### Client
|
||||
|
||||
- :white_check_mark: [Web](https://github.com/appwrite/sdk-for-web) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Flutter](https://github.com/appwrite/sdk-for-flutter) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Apple](https://github.com/appwrite/sdk-for-apple) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Android](https://github.com/appwrite/sdk-for-android) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [React Native](https://github.com/appwrite/sdk-for-react-native) - **Beta** (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Web](https://github.com/appwrite/sdk-for-web)
|
||||
- :white_check_mark: [Flutter](https://github.com/appwrite/sdk-for-flutter)
|
||||
- :white_check_mark: [Apple](https://github.com/appwrite/sdk-for-apple)
|
||||
- :white_check_mark: [Android](https://github.com/appwrite/sdk-for-android)
|
||||
- :white_check_mark: [React Native](https://github.com/appwrite/sdk-for-react-native)
|
||||
|
||||
#### Server
|
||||
|
||||
- :white_check_mark: [NodeJS](https://github.com/appwrite/sdk-for-node) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [PHP](https://github.com/appwrite/sdk-for-php) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Dart](https://github.com/appwrite/sdk-for-dart) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Deno](https://github.com/appwrite/sdk-for-deno) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Ruby](https://github.com/appwrite/sdk-for-ruby) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Python](https://github.com/appwrite/sdk-for-python) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Kotlin](https://github.com/appwrite/sdk-for-kotlin) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Swift](https://github.com/appwrite/sdk-for-swift) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [.NET](https://github.com/appwrite/sdk-for-dotnet) - **Beta** (Maintained by the Appwrite Team)
|
||||
|
||||
#### Community
|
||||
|
||||
- :white_check_mark: [Appcelerator Titanium](https://github.com/m1ga/ti.appwrite) (Maintained by [Michael Gangolf](https://github.com/m1ga/))
|
||||
- :white_check_mark: [Godot Engine](https://github.com/GodotNuts/appwrite-sdk) (Maintained by [fenix-hub @GodotNuts](https://github.com/fenix-hub))
|
||||
- :white_check_mark: [NodeJS](https://github.com/appwrite/sdk-for-node)
|
||||
- :white_check_mark: [PHP](https://github.com/appwrite/sdk-for-php)
|
||||
- :white_check_mark: [Dart](https://github.com/appwrite/sdk-for-dart)
|
||||
- :white_check_mark: [Deno](https://github.com/appwrite/sdk-for-deno)
|
||||
- :white_check_mark: [Ruby](https://github.com/appwrite/sdk-for-ruby)
|
||||
- :white_check_mark: [Python](https://github.com/appwrite/sdk-for-python)
|
||||
- :white_check_mark: [Kotlin](https://github.com/appwrite/sdk-for-kotlin)
|
||||
- :white_check_mark: [Swift](https://github.com/appwrite/sdk-for-swift)
|
||||
- :white_check_mark: [.NET](https://github.com/appwrite/sdk-for-dotnet)
|
||||
|
||||
Looking for more SDKs? - Help us by contributing a pull request to our [SDK Generator](https://github.com/appwrite/sdk-generator)!
|
||||
|
||||
|
||||
+43
-42
@@ -6,8 +6,8 @@ use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\StatsResources;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
use Appwrite\Runtimes\Runtimes;
|
||||
use Appwrite\Usage\Context as UsageContext;
|
||||
@@ -18,13 +18,15 @@ use Swoole\Timer;
|
||||
use Utopia\Cache\Adapter\Pool as CachePool;
|
||||
use Utopia\Cache\Adapter\Sharding;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\CLI\Adapters\Generic;
|
||||
use Utopia\CLI\CLI;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Adapter\Pool as DatabasePool;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DI\Dependency;
|
||||
use Utopia\DI\Container;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Platform\Service;
|
||||
@@ -47,7 +49,7 @@ require_once __DIR__ . '/controllers/general.php';
|
||||
global $register;
|
||||
|
||||
$platform = new Appwrite();
|
||||
$args = $platform->getEnv('argv');
|
||||
$args = $_SERVER['argv'] ?? [];
|
||||
|
||||
\array_shift($args);
|
||||
if (! isset($args[0])) {
|
||||
@@ -56,21 +58,15 @@ if (! isset($args[0])) {
|
||||
}
|
||||
|
||||
$taskName = $args[0];
|
||||
$container = new Container();
|
||||
$cli = new CLI(new Generic(), $_SERVER['argv'] ?? [], $container);
|
||||
|
||||
$platform->setCli($cli);
|
||||
$platform->init(Service::TYPE_TASK);
|
||||
$cli = $platform->getCli();
|
||||
|
||||
$setResource = function (string $name, callable $callback, array $injections = []) use ($cli) {
|
||||
$dependency = new Dependency();
|
||||
$dependency->setName($name)->setCallback($callback);
|
||||
foreach ($injections as $injection) {
|
||||
$dependency->inject($injection);
|
||||
}
|
||||
$cli->setResource($dependency);
|
||||
};
|
||||
$container->set('register', fn () => $register, []);
|
||||
|
||||
$setResource('register', fn () => $register, []);
|
||||
|
||||
$setResource('cache', function ($pools) {
|
||||
$container->set('cache', function ($pools) {
|
||||
$list = Config::getParam('pools-cache', []);
|
||||
$adapters = [];
|
||||
|
||||
@@ -81,18 +77,18 @@ $setResource('cache', function ($pools) {
|
||||
return new Cache(new Sharding($adapters));
|
||||
}, ['pools']);
|
||||
|
||||
$setResource('pools', function (Registry $register) {
|
||||
$container->set('pools', function (Registry $register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
|
||||
$setResource('authorization', function () {
|
||||
$container->set('authorization', function () {
|
||||
$authorization = new Authorization();
|
||||
$authorization->disable();
|
||||
|
||||
return $authorization;
|
||||
}, []);
|
||||
|
||||
$setResource('dbForPlatform', function ($pools, $cache, $authorization) {
|
||||
$container->set('dbForPlatform', function ($pools, $cache, $authorization) {
|
||||
$sleep = 3;
|
||||
$maxAttempts = 5;
|
||||
$attempts = 0;
|
||||
@@ -135,17 +131,17 @@ $setResource('dbForPlatform', function ($pools, $cache, $authorization) {
|
||||
return $dbForPlatform;
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
$setResource('console', function () {
|
||||
$container->set('console', function () {
|
||||
return new Document(Config::getParam('console'));
|
||||
}, []);
|
||||
|
||||
$setResource(
|
||||
$container->set(
|
||||
'isResourceBlocked',
|
||||
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false,
|
||||
[]
|
||||
);
|
||||
|
||||
$setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) {
|
||||
$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) {
|
||||
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
|
||||
|
||||
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) {
|
||||
@@ -207,10 +203,10 @@ $setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $c
|
||||
};
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
|
||||
|
||||
$setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
$database = null;
|
||||
|
||||
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
|
||||
return function (?Document $project = null) use ($pools, $cache, &$database, $authorization) {
|
||||
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant($project->getSequence());
|
||||
return $database;
|
||||
@@ -235,41 +231,42 @@ $setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $a
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
$setResource('publisher', function (Group $pools) {
|
||||
$container->set('publisher', function (Group $pools) {
|
||||
return new BrokerPool(publisher: $pools->get('publisher'));
|
||||
}, ['pools']);
|
||||
$setResource('publisherDatabases', function (BrokerPool $publisher) {
|
||||
$container->set('publisherDatabases', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
$setResource('publisherFunctions', function (BrokerPool $publisher) {
|
||||
$container->set('publisherFunctions', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
$setResource('publisherMigrations', function (BrokerPool $publisher) {
|
||||
$container->set('publisherMigrations', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
$setResource('publisherMessaging', function (BrokerPool $publisher) {
|
||||
$container->set('publisherMessaging', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
$setResource('usage', function () {
|
||||
$container->set('usage', function () {
|
||||
return new UsageContext();
|
||||
}, []);
|
||||
$setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
|
||||
$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$setResource('queueForStatsResources', function (Publisher $publisher) {
|
||||
return new StatsResources($publisher);
|
||||
}, ['publisher']);
|
||||
$setResource('queueForFunctions', function (Publisher $publisher) {
|
||||
$container->set('publisherForStatsResources', fn (Publisher $publisher) => new StatsResourcesPublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$container->set('queueForFunctions', function (Publisher $publisher) {
|
||||
return new Func($publisher);
|
||||
}, ['publisher']);
|
||||
$setResource('queueForDeletes', function (Publisher $publisher) {
|
||||
$container->set('queueForDeletes', function (Publisher $publisher) {
|
||||
return new Delete($publisher);
|
||||
}, ['publisher']);
|
||||
$setResource('queueForCertificates', function (Publisher $publisher) {
|
||||
$container->set('queueForCertificates', function (Publisher $publisher) {
|
||||
return new Certificate($publisher);
|
||||
}, ['publisher']);
|
||||
$setResource('logError', function (Registry $register) {
|
||||
$container->set('logError', function (Registry $register) {
|
||||
return function (Throwable $error, string $namespace, string $action) use ($register) {
|
||||
Console::error('[Error] Timestamp: ' . date('c', time()));
|
||||
Console::error('[Error] Type: ' . get_class($error));
|
||||
@@ -321,25 +318,28 @@ $setResource('logError', function (Registry $register) {
|
||||
};
|
||||
}, ['register']);
|
||||
|
||||
$setResource('executor', fn () => new Executor(), []);
|
||||
$container->set('executor', fn () => new Executor(), []);
|
||||
|
||||
$setResource('bus', function (Registry $register) use ($cli) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $cli->getResource($name));
|
||||
$container->set('bus', function (Registry $register) use ($container) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $container->get($name));
|
||||
}, ['register']);
|
||||
|
||||
$setResource('telemetry', fn () => new NoTelemetry(), []);
|
||||
$container->set('telemetry', fn () => new NoTelemetry(), []);
|
||||
|
||||
$exitCode = 0;
|
||||
|
||||
$cli
|
||||
->error()
|
||||
->inject('error')
|
||||
->inject('logError')
|
||||
->action(function (Throwable $error, callable $logError) use ($taskName) {
|
||||
->action(function (Throwable $error, callable $logError) use ($taskName, &$exitCode) {
|
||||
call_user_func_array($logError, [
|
||||
$error,
|
||||
'Task',
|
||||
$taskName,
|
||||
]);
|
||||
|
||||
$exitCode = 1;
|
||||
Timer::clearAll();
|
||||
});
|
||||
|
||||
@@ -348,3 +348,4 @@ $cli->shutdown()->action(fn () => Timer::clearAll());
|
||||
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
|
||||
require_once __DIR__ . '/init/span.php';
|
||||
run($cli->run(...));
|
||||
Console::exit($exitCode);
|
||||
|
||||
@@ -594,7 +594,7 @@ $platformCollections = [
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('key'),
|
||||
'$id' => ID::custom('key'), // For app platforms
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
@@ -605,7 +605,7 @@ $platformCollections = [
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('store'),
|
||||
'$id' => ID::custom('store'), // Unused at the moment
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 256,
|
||||
@@ -616,7 +616,7 @@ $platformCollections = [
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('hostname'),
|
||||
'$id' => ID::custom('hostname'), // For web platforms
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 256,
|
||||
|
||||
@@ -39,6 +39,10 @@ $console = [
|
||||
'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user
|
||||
'duration' => TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
|
||||
'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled',
|
||||
// For email configuration, false means feature is disabled; false means these emails are allowed during sign-ups
|
||||
'disposableEmails' => false,
|
||||
'canonicalEmails' => false,
|
||||
'freeEmails' => false,
|
||||
'invalidateSessions' => true
|
||||
],
|
||||
'authWhitelistEmails' => (!empty(System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [],
|
||||
|
||||
@@ -226,6 +226,21 @@ return [
|
||||
'description' => 'A user with the same email already exists in the current project.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::USER_EMAIL_DISPOSABLE => [
|
||||
'name' => Exception::USER_EMAIL_DISPOSABLE,
|
||||
'description' => 'Disposable email addresses are not allowed. Please use a permanent email address.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::USER_EMAIL_FREE => [
|
||||
'name' => Exception::USER_EMAIL_FREE,
|
||||
'description' => 'Free email addresses are not allowed. Please use a business or custom-domain email address.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::USER_EMAIL_NOT_CANONICAL => [
|
||||
'name' => Exception::USER_EMAIL_NOT_CANONICAL,
|
||||
'description' => 'This email address must already be in its canonical form. Please remove aliases, tags, or provider-specific variations and try again.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::USER_PASSWORD_MISMATCH => [
|
||||
'name' => Exception::USER_PASSWORD_MISMATCH,
|
||||
'description' => 'Passwords do not match. Please check the password and confirm password.',
|
||||
@@ -1164,6 +1179,16 @@ return [
|
||||
'description' => 'Platform with the requested ID could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::PLATFORM_METHOD_UNSUPPORTED => [
|
||||
'name' => Exception::PLATFORM_METHOD_UNSUPPORTED,
|
||||
'description' => 'The requested platform has invalid type. Please use corresponding update method for the platform type.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::PLATFORM_ALREADY_EXISTS => [
|
||||
'name' => Exception::PLATFORM_ALREADY_EXISTS,
|
||||
'description' => 'Platform with the same ID already exists in this project. Try again with a different ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::VARIABLE_NOT_FOUND => [
|
||||
'name' => Exception::VARIABLE_NOT_FOUND,
|
||||
'description' => 'Variable with the requested ID could not be found.',
|
||||
|
||||
@@ -376,6 +376,17 @@ return [
|
||||
'mock' => false,
|
||||
'class' => 'Appwrite\\Auth\\OAuth2\\Wordpress',
|
||||
],
|
||||
'x' => [
|
||||
'name' => 'X',
|
||||
'developers' => 'https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code',
|
||||
'icon' => 'icon-twitter',
|
||||
'enabled' => true,
|
||||
'sandbox' => false,
|
||||
'form' => false,
|
||||
'beta' => false,
|
||||
'mock' => false,
|
||||
'class' => 'Appwrite\\Auth\\OAuth2\\X',
|
||||
],
|
||||
'yahoo' => [
|
||||
'name' => 'Yahoo',
|
||||
'developers' => 'https://developer.yahoo.com/oauth2/guide/flows_authcode/',
|
||||
|
||||
@@ -9,8 +9,8 @@ return [
|
||||
'key' => 'graphql',
|
||||
'name' => 'GraphQL',
|
||||
],
|
||||
'realtime' => [
|
||||
'key' => 'realtime',
|
||||
'name' => 'Realtime',
|
||||
'websocket' => [
|
||||
'key' => 'websocket',
|
||||
'name' => 'Websocket',
|
||||
],
|
||||
];
|
||||
@@ -3,13 +3,6 @@
|
||||
// List of scopes for organization (teams) API keys
|
||||
|
||||
return [
|
||||
"platforms.read" => [
|
||||
"description" => 'Access to read project\'s platforms',
|
||||
],
|
||||
"platforms.write" => [
|
||||
"description" =>
|
||||
'Access to create, update, and delete project\'s platforms',
|
||||
],
|
||||
"projects.read" => [
|
||||
"description" => 'Access to read organization\'s projects',
|
||||
],
|
||||
@@ -17,13 +10,6 @@ return [
|
||||
"description" =>
|
||||
"Access to create, update, and delete projects in organization",
|
||||
],
|
||||
"keys.read" => [
|
||||
"description" => 'Access to read project\'s API keys',
|
||||
],
|
||||
"keys.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s API keys",
|
||||
],
|
||||
"devKeys.read" => [
|
||||
"description" => 'Access to read project\'s development keys',
|
||||
],
|
||||
|
||||
@@ -188,4 +188,20 @@ return [ // List of publicly visible scopes
|
||||
"description" =>
|
||||
"Access to update project\'s information",
|
||||
],
|
||||
"keys.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s keys",
|
||||
],
|
||||
"keys.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s keys",
|
||||
],
|
||||
"platforms.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s platforms",
|
||||
],
|
||||
"platforms.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s platforms",
|
||||
],
|
||||
];
|
||||
|
||||
@@ -137,7 +137,7 @@ return [
|
||||
'docs' => true,
|
||||
'docsUrl' => '',
|
||||
'tests' => false,
|
||||
'optional' => false,
|
||||
'optional' => true,
|
||||
'icon' => '',
|
||||
'platforms' => ['client', 'server', 'console'],
|
||||
],
|
||||
@@ -193,7 +193,7 @@ return [
|
||||
'docs' => false,
|
||||
'docsUrl' => '',
|
||||
'tests' => false,
|
||||
'optional' => false,
|
||||
'optional' => true,
|
||||
'icon' => '',
|
||||
'platforms' => ['client', 'server', 'console'],
|
||||
],
|
||||
@@ -235,7 +235,7 @@ return [
|
||||
'docs' => true,
|
||||
'docsUrl' => 'https://appwrite.io/docs/proxy',
|
||||
'tests' => false,
|
||||
'optional' => false,
|
||||
'optional' => true,
|
||||
'icon' => '/images/services/proxy.png',
|
||||
'platforms' => ['client', 'server', 'console'],
|
||||
],
|
||||
@@ -291,7 +291,7 @@ return [
|
||||
'docs' => true,
|
||||
'docsUrl' => 'https://appwrite.io/docs/migrations',
|
||||
'tests' => true,
|
||||
'optional' => false,
|
||||
'optional' => true,
|
||||
'icon' => '/images/services/migrations.png',
|
||||
'platforms' => ['client', 'server', 'console'],
|
||||
],
|
||||
|
||||
@@ -31,9 +31,6 @@ class FunctionUseCases
|
||||
public const DEV_TOOLS = 'dev-tools';
|
||||
public const AUTH = 'auth';
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
public static function getAll(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -25,9 +25,6 @@ class SiteUseCases
|
||||
public const FORMS = 'forms';
|
||||
public const DASHBOARD = 'dashboard';
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
public static function getAll(): array
|
||||
{
|
||||
return [
|
||||
@@ -252,7 +249,7 @@ return [
|
||||
'frameworks' => [
|
||||
getFramework('VITE', [
|
||||
'providerRootDirectory' => './vite/vitepress',
|
||||
'outputDirectory' => '404.html',
|
||||
'fallbackFile' => '404.html',
|
||||
'installCommand' => 'npm i vitepress && npm install',
|
||||
'buildCommand' => 'npm run docs:build',
|
||||
'outputDirectory' => './.vitepress/dist',
|
||||
@@ -275,7 +272,7 @@ return [
|
||||
'frameworks' => [
|
||||
getFramework('VUE', [
|
||||
'providerRootDirectory' => './vue/vuepress',
|
||||
'outputDirectory' => '404.html',
|
||||
'fallbackFile' => '404.html',
|
||||
'installCommand' => 'npm install',
|
||||
'buildCommand' => 'npm run build',
|
||||
'outputDirectory' => './src/.vuepress/dist',
|
||||
@@ -298,7 +295,7 @@ return [
|
||||
'frameworks' => [
|
||||
getFramework('REACT', [
|
||||
'providerRootDirectory' => './react/docusaurus',
|
||||
'outputDirectory' => '404.html',
|
||||
'fallbackFile' => '404.html',
|
||||
'installCommand' => 'npm install',
|
||||
'buildCommand' => 'npm run build',
|
||||
'outputDirectory' => './build',
|
||||
|
||||
@@ -872,18 +872,18 @@ return [
|
||||
],
|
||||
[
|
||||
'name' => '_APP_FUNCTIONS_BUILD_TIMEOUT',
|
||||
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 900 seconds.',
|
||||
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 2700 seconds.',
|
||||
'introduction' => '0.13.0',
|
||||
'default' => '900',
|
||||
'default' => '2700',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_COMPUTE_BUILD_TIMEOUT',
|
||||
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 900 seconds.',
|
||||
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 2700 seconds.',
|
||||
'introduction' => '1.7.0',
|
||||
'default' => '900',
|
||||
'default' => '2700',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
|
||||
+316
-262
@@ -9,6 +9,7 @@ use Appwrite\Auth\Validator\PasswordDictionary;
|
||||
use Appwrite\Auth\Validator\PasswordHistory;
|
||||
use Appwrite\Auth\Validator\PersonalData;
|
||||
use Appwrite\Auth\Validator\Phone;
|
||||
use Appwrite\Bus\Events\SessionCreated;
|
||||
use Appwrite\Detector\Detector;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
@@ -41,6 +42,7 @@ use Utopia\Auth\Proofs\Code as ProofsCode;
|
||||
use Utopia\Auth\Proofs\Password as ProofsPassword;
|
||||
use Utopia\Auth\Proofs\Token as ProofsToken;
|
||||
use Utopia\Auth\Store;
|
||||
use Utopia\Bus\Bus;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
@@ -75,139 +77,8 @@ use Utopia\Validator\WhiteList;
|
||||
$oauthDefaultSuccess = '/console/auth/oauth2/success';
|
||||
$oauthDefaultFailure = '/console/auth/oauth2/failure';
|
||||
|
||||
function sendSessionAlert(Locale $locale, Document $user, Document $project, array $platform, Document $session, Mail $queueForMails)
|
||||
{
|
||||
$subject = $locale->getText("emails.sessionAlert.subject");
|
||||
$preview = $locale->getText("emails.sessionAlert.preview");
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.sessionAlert-' . $locale->default] ?? [];
|
||||
$smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base');
|
||||
|
||||
$validator = new FileName();
|
||||
if (!$validator->isValid($smtpBaseTemplate)) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid template path');
|
||||
}
|
||||
|
||||
$bodyTemplate = __DIR__ . '/../../config/locale/templates/' . $smtpBaseTemplate . '.tpl';
|
||||
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-session-alert.tpl');
|
||||
$message
|
||||
->setParam('{{hello}}', $locale->getText("emails.sessionAlert.hello"))
|
||||
->setParam('{{body}}', $locale->getText("emails.sessionAlert.body"))
|
||||
->setParam('{{listDevice}}', $locale->getText("emails.sessionAlert.listDevice"))
|
||||
->setParam('{{listIpAddress}}', $locale->getText("emails.sessionAlert.listIpAddress"))
|
||||
->setParam('{{listCountry}}', $locale->getText("emails.sessionAlert.listCountry"))
|
||||
->setParam('{{footer}}', $locale->getText("emails.sessionAlert.footer"))
|
||||
->setParam('{{thanks}}', $locale->getText("emails.sessionAlert.thanks"))
|
||||
->setParam('{{signature}}', $locale->getText("emails.sessionAlert.signature"));
|
||||
|
||||
$body = $message->render();
|
||||
|
||||
$smtp = $project->getAttribute('smtp', []);
|
||||
$smtpEnabled = $smtp['enabled'] ?? false;
|
||||
|
||||
$senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
|
||||
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
|
||||
$replyTo = "";
|
||||
|
||||
if ($smtpEnabled) {
|
||||
if (!empty($smtp['senderEmail'])) {
|
||||
$senderEmail = $smtp['senderEmail'];
|
||||
}
|
||||
if (!empty($smtp['senderName'])) {
|
||||
$senderName = $smtp['senderName'];
|
||||
}
|
||||
if (!empty($smtp['replyTo'])) {
|
||||
$replyTo = $smtp['replyTo'];
|
||||
}
|
||||
|
||||
$queueForMails
|
||||
->setSmtpHost($smtp['host'] ?? '')
|
||||
->setSmtpPort($smtp['port'] ?? '')
|
||||
->setSmtpUsername($smtp['username'] ?? '')
|
||||
->setSmtpPassword($smtp['password'] ?? '')
|
||||
->setSmtpSecure($smtp['secure'] ?? '');
|
||||
|
||||
if (!empty($customTemplate)) {
|
||||
if (!empty($customTemplate['senderEmail'])) {
|
||||
$senderEmail = $customTemplate['senderEmail'];
|
||||
}
|
||||
if (!empty($customTemplate['senderName'])) {
|
||||
$senderName = $customTemplate['senderName'];
|
||||
}
|
||||
if (!empty($customTemplate['replyTo'])) {
|
||||
$replyTo = $customTemplate['replyTo'];
|
||||
}
|
||||
|
||||
$body = $customTemplate['message'] ?? '';
|
||||
$subject = $customTemplate['subject'] ?? $subject;
|
||||
}
|
||||
|
||||
$queueForMails
|
||||
->setSmtpReplyTo($replyTo)
|
||||
->setSmtpSenderEmail($senderEmail)
|
||||
->setSmtpSenderName($senderName);
|
||||
}
|
||||
|
||||
// session alerts should always have a client name!
|
||||
$clientName = $session->getAttribute('clientName');
|
||||
if (empty($clientName)) {
|
||||
// fallback to the user agent and then unknown!
|
||||
$userAgent = $session->getAttribute('userAgent');
|
||||
$clientName = !empty($userAgent) ? $userAgent : 'UNKNOWN';
|
||||
|
||||
$session->setAttribute('clientName', $clientName);
|
||||
}
|
||||
|
||||
$projectName = $project->getAttribute('name');
|
||||
if ($project->getId() === 'console') {
|
||||
$projectName = $platform['platformName'];
|
||||
}
|
||||
|
||||
$emailVariables = [
|
||||
'direction' => $locale->getText('settings.direction'),
|
||||
'date' => (new \DateTime())->format('F j'),
|
||||
'year' => (new \DateTime())->format('YYYY'),
|
||||
'time' => (new \DateTime())->format('H:i:s'),
|
||||
'user' => $user->getAttribute('name'),
|
||||
'project' => $projectName,
|
||||
'device' => $session->getAttribute('clientName'),
|
||||
'ipAddress' => $session->getAttribute('ip'),
|
||||
'country' => $locale->getText('countries.' . $session->getAttribute('countryCode'), $locale->getText('locale.country.unknown')),
|
||||
];
|
||||
|
||||
if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) {
|
||||
$emailVariables = array_merge($emailVariables, [
|
||||
'accentColor' => $platform['accentColor'],
|
||||
'logoUrl' => $platform['logoUrl'],
|
||||
'twitter' => $platform['twitterUrl'],
|
||||
'discord' => $platform['discordUrl'],
|
||||
'github' => $platform['githubUrl'],
|
||||
'terms' => $platform['termsUrl'],
|
||||
'privacy' => $platform['privacyUrl'],
|
||||
'platform' => $platform['platformName'],
|
||||
]);
|
||||
}
|
||||
|
||||
$email = $user->getAttribute('email');
|
||||
|
||||
$queueForMails
|
||||
->setSubject($subject)
|
||||
->setPreview($preview)
|
||||
->setBody($body)
|
||||
->setBodyTemplate($bodyTemplate)
|
||||
->appendVariables($emailVariables)
|
||||
->setRecipient($email);
|
||||
|
||||
// since this is console project, set email sender name!
|
||||
if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) {
|
||||
$queueForMails->setSenderName($platform['emailSenderName']);
|
||||
}
|
||||
|
||||
$queueForMails->trigger();
|
||||
}
|
||||
|
||||
|
||||
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, Authorization $authorization) {
|
||||
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Bus $bus, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) {
|
||||
|
||||
// Attempt to decode secret as a JWT (used by OAuth2 token flow to carry provider info)
|
||||
$oauthProvider = null;
|
||||
@@ -318,23 +189,12 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed saving user to DB');
|
||||
}
|
||||
|
||||
$isAllowedTokenType = match ($verifiedToken->getAttribute('type')) {
|
||||
TOKEN_TYPE_MAGIC_URL,
|
||||
TOKEN_TYPE_EMAIL => false,
|
||||
default => true
|
||||
};
|
||||
|
||||
$hasUserEmail = $user->getAttribute('email', false) !== false;
|
||||
|
||||
$isSessionAlertsEnabled = $project->getAttribute('auths', [])['sessionAlerts'] ?? false;
|
||||
|
||||
$isNotFirstSession = $dbForProject->count('sessions', [
|
||||
Query::equal('userId', [$user->getId()]),
|
||||
]) !== 1;
|
||||
|
||||
if ($isAllowedTokenType && $hasUserEmail && $isSessionAlertsEnabled && $isNotFirstSession) {
|
||||
sendSessionAlert($locale, $user, $project, $platform, $session, $queueForMails);
|
||||
}
|
||||
$bus->dispatch(new SessionCreated(
|
||||
user: $user->getArrayCopy(),
|
||||
project: $project->getArrayCopy(),
|
||||
session: $session->getArrayCopy(),
|
||||
locale: $locale->default,
|
||||
));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
@@ -345,7 +205,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||
->setProperty('secret', $sessionSecret)
|
||||
->encode();
|
||||
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
if (!$domainVerification) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
|
||||
}
|
||||
|
||||
@@ -353,8 +213,8 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||
$protocol = $request->getProtocol();
|
||||
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED);
|
||||
|
||||
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
|
||||
@@ -403,7 +263,8 @@ Http::post('/v1/account')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Authorization $authorization, Hooks $hooks) {
|
||||
->inject('plan')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Authorization $authorization, Hooks $hooks, array $plan) {
|
||||
|
||||
$email = \strtolower($email);
|
||||
if ('console' === $project->getId()) {
|
||||
@@ -452,11 +313,38 @@ Http::post('/v1/account')
|
||||
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||
$proof = new ProofsPassword();
|
||||
$hash = $proof->hash($password);
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => null,
|
||||
'emailIsCanonical' => null,
|
||||
'emailIsCorporate' => null,
|
||||
'emailIsDisposable' => null,
|
||||
'emailIsFree' => null,
|
||||
];
|
||||
|
||||
try {
|
||||
$emailCanonical = new Email($email);
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
$parsedEmail = new Email($email);
|
||||
$canonical = $parsedEmail->getCanonical();
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => $canonical,
|
||||
'emailIsCanonical' => $parsedEmail->get() === $canonical,
|
||||
'emailIsCorporate' => $parsedEmail->isCorporate(),
|
||||
'emailIsDisposable' => $parsedEmail->isDisposable(),
|
||||
'emailIsFree' => $parsedEmail->isFree(),
|
||||
];
|
||||
} catch (\Throwable) {
|
||||
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
|
||||
}
|
||||
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
|
||||
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
|
||||
}
|
||||
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
|
||||
}
|
||||
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
|
||||
throw new Exception(Exception::USER_EMAIL_FREE);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -487,11 +375,11 @@ Http::post('/v1/account')
|
||||
'authenticators' => null,
|
||||
'search' => implode(' ', [$userId, $email, $name]),
|
||||
'accessedAt' => DateTime::now(),
|
||||
'emailCanonical' => $emailCanonical?->getCanonical(),
|
||||
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
|
||||
'emailIsCorporate' => $emailCanonical?->isCorporate(),
|
||||
'emailIsDisposable' => $emailCanonical?->isDisposable(),
|
||||
'emailIsFree' => $emailCanonical?->isFree(),
|
||||
'emailCanonical' => $emailMetadata['emailCanonical'],
|
||||
'emailIsCanonical' => $emailMetadata['emailIsCanonical'],
|
||||
'emailIsCorporate' => $emailMetadata['emailIsCorporate'],
|
||||
'emailIsDisposable' => $emailMetadata['emailIsDisposable'],
|
||||
'emailIsFree' => $emailMetadata['emailIsFree'],
|
||||
]);
|
||||
|
||||
$user->removeAttribute('$sequence');
|
||||
@@ -691,15 +579,18 @@ Http::delete('/v1/account/sessions')
|
||||
->inject('queueForDeletes')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken) {
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
|
||||
|
||||
$protocol = $request->getProtocol();
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
$currentSession = null;
|
||||
|
||||
foreach ($sessions as $session) {/** @var Document $session */
|
||||
$dbForProject->deleteDocument('sessions', $session->getId());
|
||||
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
if (!$domainVerification) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
|
||||
}
|
||||
|
||||
@@ -712,10 +603,11 @@ Http::delete('/v1/account/sessions')
|
||||
|
||||
// If current session delete the cookies too
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
|
||||
// Use current session for events.
|
||||
$currentSession = $session;
|
||||
$queueForEvents
|
||||
->setPayload($response->output($session, Response::MODEL_SESSION));
|
||||
|
||||
@@ -728,9 +620,11 @@ Http::delete('/v1/account/sessions')
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
->setParam('sessionId', $session->getId());
|
||||
if ($currentSession instanceof Document) {
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
->setParam('sessionId', $currentSession->getId());
|
||||
}
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
@@ -776,7 +670,8 @@ Http::get('/v1/account/sessions/:sessionId')
|
||||
->setAttribute('secret', $session->getAttribute('secret', ''))
|
||||
;
|
||||
|
||||
return $response->dynamic($session, Response::MODEL_SESSION);
|
||||
$response->dynamic($session, Response::MODEL_SESSION);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,7 +711,9 @@ Http::delete('/v1/account/sessions/:sessionId')
|
||||
->inject('queueForDeletes')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken) {
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
|
||||
|
||||
$protocol = $request->getProtocol();
|
||||
$sessionId = ($sessionId === 'current')
|
||||
@@ -842,13 +739,13 @@ Http::delete('/v1/account/sessions/:sessionId')
|
||||
->setAttribute('current', true)
|
||||
->setAttribute('countryName', $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')));
|
||||
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
if (!$domainVerification) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
|
||||
}
|
||||
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
@@ -956,7 +853,7 @@ Http::patch('/v1/account/sessions/:sessionId')
|
||||
->setPayload($response->output($session, Response::MODEL_SESSION))
|
||||
;
|
||||
|
||||
return $response->dynamic($session, Response::MODEL_SESSION);
|
||||
$response->dynamic($session, Response::MODEL_SESSION);
|
||||
});
|
||||
|
||||
Http::post('/v1/account/sessions/email')
|
||||
@@ -997,13 +894,15 @@ Http::post('/v1/account/sessions/email')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMails')
|
||||
->inject('bus')
|
||||
->inject('hooks')
|
||||
->inject('store')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->inject('authorization')
|
||||
->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) {
|
||||
->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Bus $bus, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) {
|
||||
$email = \strtolower($email);
|
||||
$protocol = $request->getProtocol();
|
||||
|
||||
@@ -1064,28 +963,28 @@ Http::post('/v1/account/sessions/email')
|
||||
]));
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$session = $dbForProject->createDocument('sessions', $session->setAttribute('$permissions', [
|
||||
Permission::read(Role::user($user->getId())),
|
||||
Permission::update(Role::user($user->getId())),
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
]));
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$encoded = $store
|
||||
->setProperty('id', $user->getId())
|
||||
->setProperty('secret', $secret)
|
||||
->encode();
|
||||
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
if (!$domainVerification) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
|
||||
}
|
||||
|
||||
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
|
||||
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
;
|
||||
|
||||
@@ -1102,15 +1001,12 @@ Http::post('/v1/account/sessions/email')
|
||||
->setParam('sessionId', $session->getId())
|
||||
;
|
||||
|
||||
if ($project->getAttribute('auths', [])['sessionAlerts'] ?? false) {
|
||||
if (
|
||||
$dbForProject->count('sessions', [
|
||||
Query::equal('userId', [$user->getId()]),
|
||||
]) !== 1
|
||||
) {
|
||||
sendSessionAlert($locale, $user, $project, $platform, $session, $queueForMails);
|
||||
}
|
||||
}
|
||||
$bus->dispatch(new SessionCreated(
|
||||
user: $user->getArrayCopy(),
|
||||
project: $project->getArrayCopy(),
|
||||
session: $session->getArrayCopy(),
|
||||
locale: $locale->default,
|
||||
));
|
||||
|
||||
$response->dynamic($session, Response::MODEL_SESSION);
|
||||
});
|
||||
@@ -1151,8 +1047,10 @@ Http::post('/v1/account/sessions/anonymous')
|
||||
->inject('store')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->inject('authorization')
|
||||
->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) {
|
||||
->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) {
|
||||
$protocol = $request->getProtocol();
|
||||
|
||||
if ('console' === $project->getId()) {
|
||||
@@ -1243,15 +1141,15 @@ Http::post('/v1/account/sessions/anonymous')
|
||||
->setProperty('secret', $secret)
|
||||
->encode();
|
||||
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
if (!$domainVerification) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
|
||||
}
|
||||
|
||||
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
|
||||
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
;
|
||||
|
||||
@@ -1302,11 +1200,13 @@ Http::post('/v1/account/sessions/token')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMails')
|
||||
->inject('bus')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->inject('proofForCode')
|
||||
->inject('authorization')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->inject('authorization')
|
||||
->action($createSession);
|
||||
|
||||
Http::get('/v1/account/sessions/oauth2/:provider')
|
||||
@@ -1504,8 +1404,11 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->inject('store')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->inject('plan')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->inject('authorization')
|
||||
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) use ($oauthDefaultSuccess) {
|
||||
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) use ($oauthDefaultSuccess) {
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$port = $request->getPort();
|
||||
$callbackBase = $protocol . '://' . $request->getHostname();
|
||||
@@ -1747,10 +1650,38 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
}
|
||||
}
|
||||
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => null,
|
||||
'emailIsCanonical' => null,
|
||||
'emailIsCorporate' => null,
|
||||
'emailIsDisposable' => null,
|
||||
'emailIsFree' => null,
|
||||
];
|
||||
|
||||
try {
|
||||
$emailCanonical = new Email($email);
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
$parsedEmail = new Email($email);
|
||||
$canonical = $parsedEmail->getCanonical();
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => $canonical,
|
||||
'emailIsCanonical' => $parsedEmail->get() === $canonical,
|
||||
'emailIsCorporate' => $parsedEmail->isCorporate(),
|
||||
'emailIsDisposable' => $parsedEmail->isDisposable(),
|
||||
'emailIsFree' => $parsedEmail->isFree(),
|
||||
];
|
||||
} catch (\Throwable) {
|
||||
$failureRedirect(Exception::GENERAL_INVALID_EMAIL);
|
||||
}
|
||||
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
|
||||
$failureRedirect(Exception::USER_EMAIL_DISPOSABLE);
|
||||
}
|
||||
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
|
||||
$failureRedirect(Exception::USER_EMAIL_NOT_CANONICAL);
|
||||
}
|
||||
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
|
||||
$failureRedirect(Exception::USER_EMAIL_FREE);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1780,11 +1711,11 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
'authenticators' => null,
|
||||
'search' => implode(' ', [$userId, $email, $name]),
|
||||
'accessedAt' => DateTime::now(),
|
||||
'emailCanonical' => $emailCanonical?->getCanonical(),
|
||||
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
|
||||
'emailIsCorporate' => $emailCanonical?->isCorporate(),
|
||||
'emailIsDisposable' => $emailCanonical?->isDisposable(),
|
||||
'emailIsFree' => $emailCanonical?->isFree(),
|
||||
'emailCanonical' => $emailMetadata['emailCanonical'],
|
||||
'emailIsCanonical' => $emailMetadata['emailIsCanonical'],
|
||||
'emailIsCorporate' => $emailMetadata['emailIsCorporate'],
|
||||
'emailIsDisposable' => $emailMetadata['emailIsDisposable'],
|
||||
'emailIsFree' => $emailMetadata['emailIsFree'],
|
||||
]);
|
||||
|
||||
$user->removeAttribute('$sequence');
|
||||
@@ -1859,19 +1790,47 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
}
|
||||
|
||||
if (empty($user->getAttribute('email'))) {
|
||||
$user->setAttribute('email', $oauth2->getUserEmail($accessToken));
|
||||
$email = $oauth2->getUserEmail($accessToken);
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => null,
|
||||
'emailIsCanonical' => null,
|
||||
'emailIsCorporate' => null,
|
||||
'emailIsDisposable' => null,
|
||||
'emailIsFree' => null,
|
||||
];
|
||||
|
||||
try {
|
||||
$emailCanonical = new Email($user->getAttribute('email'));
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
$parsedEmail = new Email($email);
|
||||
$canonical = $parsedEmail->getCanonical();
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => $canonical,
|
||||
'emailIsCanonical' => $parsedEmail->get() === $canonical,
|
||||
'emailIsCorporate' => $parsedEmail->isCorporate(),
|
||||
'emailIsDisposable' => $parsedEmail->isDisposable(),
|
||||
'emailIsFree' => $parsedEmail->isFree(),
|
||||
];
|
||||
} catch (\Throwable) {
|
||||
$failureRedirect(Exception::GENERAL_INVALID_EMAIL);
|
||||
}
|
||||
|
||||
$user->setAttribute('emailCanonical', $emailCanonical?->getCanonical());
|
||||
$user->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported());
|
||||
$user->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate());
|
||||
$user->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable());
|
||||
$user->setAttribute('emailIsFree', $emailCanonical?->isFree());
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
|
||||
$failureRedirect(Exception::USER_EMAIL_DISPOSABLE);
|
||||
}
|
||||
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
|
||||
$failureRedirect(Exception::USER_EMAIL_NOT_CANONICAL);
|
||||
}
|
||||
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
|
||||
$failureRedirect(Exception::USER_EMAIL_FREE);
|
||||
}
|
||||
|
||||
$user->setAttribute('email', $email);
|
||||
$user->setAttribute('emailCanonical', $emailMetadata['emailCanonical']);
|
||||
$user->setAttribute('emailIsCanonical', $emailMetadata['emailIsCanonical']);
|
||||
$user->setAttribute('emailIsCorporate', $emailMetadata['emailIsCorporate']);
|
||||
$user->setAttribute('emailIsDisposable', $emailMetadata['emailIsDisposable']);
|
||||
$user->setAttribute('emailIsFree', $emailMetadata['emailIsFree']);
|
||||
}
|
||||
|
||||
if (empty($user->getAttribute('name'))) {
|
||||
@@ -1965,7 +1924,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->setProperty('secret', $secret)
|
||||
->encode();
|
||||
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
if (!$domainVerification) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
|
||||
}
|
||||
|
||||
@@ -1978,17 +1937,17 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
// TODO: Remove this deprecated workaround - support only token
|
||||
if ($state['success']['path'] == $oauthDefaultSuccess) {
|
||||
$query['project'] = $project->getId();
|
||||
$query['domain'] = Config::getParam('cookieDomain');
|
||||
$query['domain'] = $cookieDomain;
|
||||
$query['key'] = $store->getKey();
|
||||
$query['secret'] = $encoded;
|
||||
}
|
||||
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
}
|
||||
|
||||
if (isset($sessionUpgrade) && $sessionUpgrade) {
|
||||
if (isset($sessionUpgrade) && $sessionUpgrade && isset($session)) {
|
||||
foreach ($user->getAttribute('targets', []) as $target) {
|
||||
if ($target->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) {
|
||||
continue;
|
||||
@@ -2083,7 +2042,7 @@ Http::get('/v1/account/tokens/oauth2/:provider')
|
||||
}
|
||||
|
||||
$host = $platform['consoleHostname'] ?? '';
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$port = $request->getPort();
|
||||
$redirectBase = $protocol . '://' . $host;
|
||||
if ($protocol === 'https' && $port !== '443') {
|
||||
@@ -2106,10 +2065,12 @@ Http::get('/v1/account/tokens/oauth2/:provider')
|
||||
'token' => true,
|
||||
], $scopes);
|
||||
|
||||
$loginURL = $oauth2->getLoginURL();
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->redirect($oauth2->getLoginURL());
|
||||
->redirect($loginURL);
|
||||
});
|
||||
|
||||
Http::post('/v1/account/tokens/magic-url')
|
||||
@@ -2149,10 +2110,11 @@ Http::post('/v1/account/tokens/magic-url')
|
||||
->inject('locale')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMails')
|
||||
->inject('plan')
|
||||
->inject('proofForPassword')
|
||||
->inject('platform')
|
||||
->inject('authorization')
|
||||
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) {
|
||||
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, array $plan, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) {
|
||||
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
|
||||
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
|
||||
}
|
||||
@@ -2187,10 +2149,38 @@ Http::post('/v1/account/tokens/magic-url')
|
||||
|
||||
$userId = $userId === 'unique()' ? ID::unique() : $userId;
|
||||
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => null,
|
||||
'emailIsCanonical' => null,
|
||||
'emailIsCorporate' => null,
|
||||
'emailIsDisposable' => null,
|
||||
'emailIsFree' => null,
|
||||
];
|
||||
|
||||
try {
|
||||
$emailCanonical = new Email($email);
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
$parsedEmail = new Email($email);
|
||||
$canonical = $parsedEmail->getCanonical();
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => $canonical,
|
||||
'emailIsCanonical' => $parsedEmail->get() === $canonical,
|
||||
'emailIsCorporate' => $parsedEmail->isCorporate(),
|
||||
'emailIsDisposable' => $parsedEmail->isDisposable(),
|
||||
'emailIsFree' => $parsedEmail->isFree(),
|
||||
];
|
||||
} catch (\Throwable) {
|
||||
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
|
||||
}
|
||||
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
|
||||
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
|
||||
}
|
||||
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
|
||||
}
|
||||
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
|
||||
throw new Exception(Exception::USER_EMAIL_FREE);
|
||||
}
|
||||
|
||||
$user->setAttributes([
|
||||
@@ -2217,11 +2207,11 @@ Http::post('/v1/account/tokens/magic-url')
|
||||
'authenticators' => null,
|
||||
'search' => implode(' ', [$userId, $email]),
|
||||
'accessedAt' => DateTime::now(),
|
||||
'emailCanonical' => $emailCanonical?->getCanonical(),
|
||||
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
|
||||
'emailIsCorporate' => $emailCanonical?->isCorporate(),
|
||||
'emailIsDisposable' => $emailCanonical?->isDisposable(),
|
||||
'emailIsFree' => $emailCanonical?->isFree(),
|
||||
'emailCanonical' => $emailMetadata['emailCanonical'],
|
||||
'emailIsCanonical' => $emailMetadata['emailIsCanonical'],
|
||||
'emailIsCorporate' => $emailMetadata['emailIsCorporate'],
|
||||
'emailIsDisposable' => $emailMetadata['emailIsDisposable'],
|
||||
'emailIsFree' => $emailMetadata['emailIsFree'],
|
||||
]);
|
||||
|
||||
$user->removeAttribute('$sequence');
|
||||
@@ -2429,10 +2419,11 @@ Http::post('/v1/account/tokens/email')
|
||||
->inject('locale')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMails')
|
||||
->inject('plan')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForCode')
|
||||
->inject('authorization')
|
||||
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) {
|
||||
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, array $plan, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) {
|
||||
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
|
||||
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
|
||||
}
|
||||
@@ -2465,10 +2456,38 @@ Http::post('/v1/account/tokens/email')
|
||||
|
||||
$userId = $userId === 'unique()' ? ID::unique() : $userId;
|
||||
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => null,
|
||||
'emailIsCanonical' => null,
|
||||
'emailIsCorporate' => null,
|
||||
'emailIsDisposable' => null,
|
||||
'emailIsFree' => null,
|
||||
];
|
||||
|
||||
try {
|
||||
$emailCanonical = new Email($email);
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
$parsedEmail = new Email($email);
|
||||
$canonical = $parsedEmail->getCanonical();
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => $canonical,
|
||||
'emailIsCanonical' => $parsedEmail->get() === $canonical,
|
||||
'emailIsCorporate' => $parsedEmail->isCorporate(),
|
||||
'emailIsDisposable' => $parsedEmail->isDisposable(),
|
||||
'emailIsFree' => $parsedEmail->isFree(),
|
||||
];
|
||||
} catch (\Throwable) {
|
||||
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
|
||||
}
|
||||
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
|
||||
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
|
||||
}
|
||||
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
|
||||
}
|
||||
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
|
||||
throw new Exception(Exception::USER_EMAIL_FREE);
|
||||
}
|
||||
|
||||
$user->setAttributes([
|
||||
@@ -2493,11 +2512,11 @@ Http::post('/v1/account/tokens/email')
|
||||
'memberships' => null,
|
||||
'search' => implode(' ', [$userId, $email]),
|
||||
'accessedAt' => DateTime::now(),
|
||||
'emailCanonical' => $emailCanonical?->getCanonical(),
|
||||
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
|
||||
'emailIsCorporate' => $emailCanonical?->isCorporate(),
|
||||
'emailIsDisposable' => $emailCanonical?->isDisposable(),
|
||||
'emailIsFree' => $emailCanonical?->isFree(),
|
||||
'emailCanonical' => $emailMetadata['emailCanonical'],
|
||||
'emailIsCanonical' => $emailMetadata['emailIsCanonical'],
|
||||
'emailIsCorporate' => $emailMetadata['emailIsCorporate'],
|
||||
'emailIsDisposable' => $emailMetadata['emailIsDisposable'],
|
||||
'emailIsFree' => $emailMetadata['emailIsFree'],
|
||||
]);
|
||||
|
||||
$user->removeAttribute('$sequence');
|
||||
@@ -2735,14 +2754,16 @@ Http::put('/v1/account/sessions/magic-url')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMails')
|
||||
->inject('bus')
|
||||
->inject('store')
|
||||
->inject('proofForCode')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->inject('authorization')
|
||||
->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $authorization) use ($createSession) {
|
||||
->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $bus, $store, $proofForCode, $domainVerification, $cookieDomain, $authorization) use ($createSession) {
|
||||
$proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL);
|
||||
$proofForToken->setHash(new Sha());
|
||||
$createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $authorization);
|
||||
$createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $bus, $store, $proofForToken, $proofForCode, $domainVerification, $cookieDomain, $authorization);
|
||||
});
|
||||
|
||||
Http::put('/v1/account/sessions/phone')
|
||||
@@ -2784,10 +2805,12 @@ Http::put('/v1/account/sessions/phone')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMails')
|
||||
->inject('bus')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->inject('proofForCode')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->inject('authorization')
|
||||
->action($createSession);
|
||||
|
||||
@@ -3314,9 +3337,10 @@ Http::patch('/v1/account/email')
|
||||
->inject('queueForEvents')
|
||||
->inject('project')
|
||||
->inject('hooks')
|
||||
->inject('plan')
|
||||
->inject('proofForPassword')
|
||||
->inject('authorization')
|
||||
->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, ProofsPassword $proofForPassword, Authorization $authorization) {
|
||||
->inject('authorization')
|
||||
->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, array $plan, ProofsPassword $proofForPassword, Authorization $authorization) {
|
||||
// passwordUpdate will be empty if the user has never set a password
|
||||
$passwordUpdate = $user->getAttribute('passwordUpdate');
|
||||
|
||||
@@ -3344,20 +3368,48 @@ Http::patch('/v1/account/email')
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
|
||||
}
|
||||
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => null,
|
||||
'emailIsCanonical' => null,
|
||||
'emailIsCorporate' => null,
|
||||
'emailIsDisposable' => null,
|
||||
'emailIsFree' => null,
|
||||
];
|
||||
|
||||
try {
|
||||
$emailCanonical = new Email($email);
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
$parsedEmail = new Email($email);
|
||||
$canonical = $parsedEmail->getCanonical();
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => $canonical,
|
||||
'emailIsCanonical' => $parsedEmail->get() === $canonical,
|
||||
'emailIsCorporate' => $parsedEmail->isCorporate(),
|
||||
'emailIsDisposable' => $parsedEmail->isDisposable(),
|
||||
'emailIsFree' => $parsedEmail->isFree(),
|
||||
];
|
||||
} catch (\Throwable) {
|
||||
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
|
||||
}
|
||||
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
|
||||
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
|
||||
}
|
||||
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
|
||||
}
|
||||
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
|
||||
throw new Exception(Exception::USER_EMAIL_FREE);
|
||||
}
|
||||
|
||||
$user
|
||||
->setAttribute('email', $email)
|
||||
->setAttribute('emailVerification', false) // After this user needs to confirm mail again
|
||||
->setAttribute('emailCanonical', $emailCanonical?->getCanonical())
|
||||
->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported())
|
||||
->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate())
|
||||
->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable())
|
||||
->setAttribute('emailIsFree', $emailCanonical?->isFree())
|
||||
->setAttribute('emailCanonical', $emailMetadata['emailCanonical'])
|
||||
->setAttribute('emailIsCanonical', $emailMetadata['emailIsCanonical'])
|
||||
->setAttribute('emailIsCorporate', $emailMetadata['emailIsCorporate'])
|
||||
->setAttribute('emailIsDisposable', $emailMetadata['emailIsDisposable'])
|
||||
->setAttribute('emailIsFree', $emailMetadata['emailIsFree'])
|
||||
;
|
||||
|
||||
if (empty($passwordUpdate)) {
|
||||
@@ -3550,7 +3602,9 @@ Http::patch('/v1/account/status')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('store')
|
||||
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store) {
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store, bool $domainVerification, ?string $cookieDomain) {
|
||||
|
||||
$user->setAttribute('status', false);
|
||||
|
||||
@@ -3560,14 +3614,14 @@ Http::patch('/v1/account/status')
|
||||
->setParam('userId', $user->getId())
|
||||
->setPayload($response->output($user, Response::MODEL_ACCOUNT));
|
||||
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
if (!$domainVerification) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
|
||||
}
|
||||
|
||||
$protocol = $request->getProtocol();
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
;
|
||||
|
||||
$response->dynamic($user, Response::MODEL_ACCOUNT);
|
||||
@@ -3760,7 +3814,7 @@ Http::post('/v1/account/recovery')
|
||||
->setParam('userId', $profile->getId())
|
||||
->setParam('tokenId', $recovery->getId())
|
||||
->setUser($profile)
|
||||
->setPayload(Response::showSensitive(fn () => $response->output($recovery, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
->setPayload($response->showSensitive(fn () => $response->output($recovery, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -3861,7 +3915,7 @@ Http::put('/v1/account/recovery')
|
||||
$queueForEvents
|
||||
->setParam('userId', $profile->getId())
|
||||
->setParam('tokenId', $recoveryDocument->getId())
|
||||
->setPayload(Response::showSensitive(fn () => $response->output($recoveryDocument, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
->setPayload($response->showSensitive(fn () => $response->output($recoveryDocument, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
|
||||
$response->dynamic($recoveryDocument, Response::MODEL_TOKEN);
|
||||
});
|
||||
@@ -4091,7 +4145,7 @@ Http::post('/v1/account/verifications/email')
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
->setParam('tokenId', $verification->getId())
|
||||
->setPayload(Response::showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
->setPayload($response->showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -4183,7 +4237,7 @@ Http::put('/v1/account/verifications/email')
|
||||
$queueForEvents
|
||||
->setParam('userId', $userId)
|
||||
->setParam('tokenId', $verification->getId())
|
||||
->setPayload(Response::showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
->setPayload($response->showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
|
||||
$response->dynamic($verification, Response::MODEL_TOKEN);
|
||||
});
|
||||
@@ -4715,5 +4769,5 @@ Http::delete('/v1/account/identities/:identityId')
|
||||
->setParam('identityId', $identity->getId())
|
||||
->setPayload($response->output($identity, Response::MODEL_IDENTITY));
|
||||
|
||||
return $response->noContent();
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
@@ -231,7 +231,7 @@ function execute(
|
||||
$validations = GraphQL::getStandardValidationRules();
|
||||
|
||||
if (System::getEnv('_APP_GRAPHQL_INTROSPECTION', 'enabled') === 'disabled') {
|
||||
$validations[] = new DisableIntrospection();
|
||||
$validations[] = new DisableIntrospection(DisableIntrospection::ENABLED);
|
||||
}
|
||||
|
||||
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') {
|
||||
|
||||
@@ -231,6 +231,7 @@ Http::get('/v1/locale/continents')
|
||||
->inject('locale')
|
||||
->action(function (Response $response, Locale $locale) {
|
||||
$list = array_keys(Config::getParam('locale-continents'));
|
||||
$output = [];
|
||||
|
||||
foreach ($list as $value) {
|
||||
$output[] = new Document([
|
||||
|
||||
@@ -1180,6 +1180,7 @@ Http::get('/v1/messaging/providers/:providerId/logs')
|
||||
'userEmail' => $log['data']['userEmail'] ?? null,
|
||||
'userName' => $log['data']['userName'] ?? null,
|
||||
'mode' => $log['data']['mode'] ?? null,
|
||||
'userType' => $log['data']['userType'] ?? null,
|
||||
'ip' => $log['ip'],
|
||||
'time' => $log['time'],
|
||||
'osCode' => $os['osCode'],
|
||||
@@ -2585,6 +2586,7 @@ Http::get('/v1/messaging/topics/:topicId/logs')
|
||||
'userEmail' => $log['data']['userEmail'] ?? null,
|
||||
'userName' => $log['data']['userName'] ?? null,
|
||||
'mode' => $log['data']['mode'] ?? null,
|
||||
'userType' => $log['data']['userType'] ?? null,
|
||||
'ip' => $log['ip'],
|
||||
'time' => $log['time'],
|
||||
'osCode' => $os['osCode'],
|
||||
@@ -3000,6 +3002,7 @@ Http::get('/v1/messaging/subscribers/:subscriberId/logs')
|
||||
'userEmail' => $log['data']['userEmail'] ?? null,
|
||||
'userName' => $log['data']['userName'] ?? null,
|
||||
'mode' => $log['data']['mode'] ?? null,
|
||||
'userType' => $log['data']['userType'] ?? null,
|
||||
'ip' => $log['ip'],
|
||||
'time' => $log['time'],
|
||||
'osCode' => $os['osCode'],
|
||||
@@ -3566,7 +3569,7 @@ Http::post('/v1/messaging/messages/push')
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$endpoint = "$protocol://{$platform['apiHostname']}/v1";
|
||||
|
||||
$scheduleTime = $currentScheduledAt ?? $scheduledAt;
|
||||
$scheduleTime = $scheduledAt;
|
||||
if (!\is_null($scheduleTime)) {
|
||||
$expiry = (new \DateTime($scheduleTime))->add(new \DateInterval('P15D'))->format('U');
|
||||
} else {
|
||||
@@ -3813,6 +3816,7 @@ Http::get('/v1/messaging/messages/:messageId/logs')
|
||||
'userEmail' => $log['data']['userEmail'] ?? null,
|
||||
'userName' => $log['data']['userName'] ?? null,
|
||||
'mode' => $log['data']['mode'] ?? null,
|
||||
'userType' => $log['data']['userType'] ?? null,
|
||||
'ip' => $log['ip'],
|
||||
'time' => $log['time'],
|
||||
'osCode' => $os['osCode'],
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Message\Migration as MigrationMessage;
|
||||
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\OpenSSL\OpenSSL;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -90,10 +91,9 @@ Http::post('/v1/migrations/appwrite')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (array $resources, string $endpoint, string $projectId, string $apiKey, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (array $resources, string $endpoint, string $projectId, string $apiKey, Response $response, Database $dbForProject, Document $project, array $platform, Event $queueForEvents, MigrationPublisher $publisherForMigrations) {
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
@@ -114,12 +114,11 @@ Http::post('/v1/migrations/appwrite')
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -151,10 +150,9 @@ Http::post('/v1/migrations/firebase')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (array $resources, string $serviceAccount, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (array $resources, string $serviceAccount, Response $response, Database $dbForProject, Document $project, array $platform, Event $queueForEvents, MigrationPublisher $publisherForMigrations) {
|
||||
$serviceAccountData = json_decode($serviceAccount, true);
|
||||
|
||||
if (empty($serviceAccountData)) {
|
||||
@@ -183,12 +181,11 @@ Http::post('/v1/migrations/firebase')
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -225,10 +222,9 @@ Http::post('/v1/migrations/supabase')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Event $queueForEvents, MigrationPublisher $publisherForMigrations) {
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
@@ -252,12 +248,11 @@ Http::post('/v1/migrations/supabase')
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -295,10 +290,9 @@ Http::post('/v1/migrations/nhost')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Event $queueForEvents, MigrationPublisher $publisherForMigrations) {
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
@@ -323,12 +317,11 @@ Http::post('/v1/migrations/nhost')
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -368,7 +361,7 @@ Http::post('/v1/migrations/csv/imports')
|
||||
->inject('deviceForFiles')
|
||||
->inject('deviceForMigrations')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (
|
||||
string $bucketId,
|
||||
string $fileId,
|
||||
@@ -383,7 +376,7 @@ Http::post('/v1/migrations/csv/imports')
|
||||
Device $deviceForFiles,
|
||||
Device $deviceForMigrations,
|
||||
Event $queueForEvents,
|
||||
Migration $queueForMigrations
|
||||
MigrationPublisher $publisherForMigrations
|
||||
) {
|
||||
$bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
|
||||
if ($internalFile) {
|
||||
@@ -479,11 +472,10 @@ Http::post('/v1/migrations/csv/imports')
|
||||
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -526,7 +518,7 @@ Http::post('/v1/migrations/csv/exports')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (
|
||||
string $resourceId,
|
||||
string $filename,
|
||||
@@ -545,7 +537,7 @@ Http::post('/v1/migrations/csv/exports')
|
||||
Document $project,
|
||||
array $platform,
|
||||
Event $queueForEvents,
|
||||
Migration $queueForMigrations
|
||||
MigrationPublisher $publisherForMigrations
|
||||
) {
|
||||
try {
|
||||
$parsedQueries = Query::parseQueries($queries);
|
||||
@@ -630,11 +622,11 @@ Http::post('/v1/migrations/csv/exports')
|
||||
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -673,7 +665,7 @@ Http::post('/v1/migrations/json/imports')
|
||||
->inject('deviceForFiles')
|
||||
->inject('deviceForMigrations')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (
|
||||
string $bucketId,
|
||||
string $fileId,
|
||||
@@ -688,7 +680,7 @@ Http::post('/v1/migrations/json/imports')
|
||||
Device $deviceForFiles,
|
||||
Device $deviceForMigrations,
|
||||
Event $queueForEvents,
|
||||
Migration $queueForMigrations
|
||||
MigrationPublisher $publisherForMigrations
|
||||
) {
|
||||
$bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
|
||||
if ($internalFile) {
|
||||
@@ -783,11 +775,11 @@ Http::post('/v1/migrations/json/imports')
|
||||
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -826,7 +818,7 @@ Http::post('/v1/migrations/json/exports')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (
|
||||
string $resourceId,
|
||||
string $filename,
|
||||
@@ -841,7 +833,7 @@ Http::post('/v1/migrations/json/exports')
|
||||
Document $project,
|
||||
array $platform,
|
||||
Event $queueForEvents,
|
||||
Migration $queueForMigrations
|
||||
MigrationPublisher $publisherForMigrations
|
||||
) {
|
||||
try {
|
||||
$parsedQueries = Query::parseQueries($queries);
|
||||
@@ -915,11 +907,11 @@ Http::post('/v1/migrations/json/exports')
|
||||
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
@@ -1216,9 +1208,8 @@ Http::patch('/v1/migrations/:migrationId')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('user')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (string $migrationId, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Migration $queueForMigrations) {
|
||||
->inject('publisherForMigrations')
|
||||
->action(function (string $migrationId, Response $response, Database $dbForProject, Document $project, array $platform, MigrationPublisher $publisherForMigrations) {
|
||||
$migration = $dbForProject->getDocument('migrations', $migrationId);
|
||||
|
||||
if ($migration->isEmpty()) {
|
||||
@@ -1234,12 +1225,11 @@ Http::patch('/v1/migrations/:migrationId')
|
||||
->setAttribute('dateUpdated', \time());
|
||||
|
||||
// Trigger Migration
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
$publisherForMigrations->enqueue(new MigrationMessage(
|
||||
project: $project,
|
||||
migration: $migration,
|
||||
platform: $platform,
|
||||
));
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
@@ -5,28 +5,18 @@ use Appwrite\Auth\Validator\MockNumber;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Keys;
|
||||
use Appwrite\Utopia\Response;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Http\Http;
|
||||
@@ -81,202 +71,20 @@ Http::get('/v1/projects/:projectId')
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
Http::patch('/v1/projects/:projectId/service')
|
||||
->desc('Update service status')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'projects',
|
||||
name: 'updateServiceStatus',
|
||||
description: '/docs/references/projects/update-service-status.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('service', '', new WhiteList(array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional'])), true), 'Service name.')
|
||||
->param('status', null, new Boolean(), 'Service status.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $service, bool $status, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$services = $project->getAttribute('services', []);
|
||||
$services[$service] = $status;
|
||||
|
||||
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('services', $services));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
Http::patch('/v1/projects/:projectId/service/all')
|
||||
->desc('Update all service status')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'projects',
|
||||
name: 'updateServiceStatusAll',
|
||||
description: '/docs/references/projects/update-service-status-all.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('status', null, new Boolean(), 'Service status.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, bool $status, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$allServices = array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional']));
|
||||
|
||||
$services = [];
|
||||
foreach ($allServices as $service) {
|
||||
$services[$service] = $status;
|
||||
}
|
||||
|
||||
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('services', $services));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
Http::patch('/v1/projects/:projectId/api')
|
||||
->desc('Update API status')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', [
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
group: 'projects',
|
||||
name: 'updateApiStatus',
|
||||
description: '/docs/references/projects/update-api-status.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
],
|
||||
deprecated: new Deprecated(
|
||||
since: '1.8.0',
|
||||
replaceWith: 'projects.updateAPIStatus',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
group: 'projects',
|
||||
name: 'updateAPIStatus',
|
||||
description: '/docs/references/projects/update-api-status.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
]
|
||||
)
|
||||
])
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('api', '', new WhiteList(array_keys(Config::getParam('apis')), true), 'API name.')
|
||||
->param('status', null, new Boolean(), 'API status.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $api, bool $status, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$apis = $project->getAttribute('apis', []);
|
||||
$apis[$api] = $status;
|
||||
|
||||
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('apis', $apis));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
->action(function () {
|
||||
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED, 'Bulk API no longer exists for services. Please change status individually.');
|
||||
});
|
||||
|
||||
Http::patch('/v1/projects/:projectId/api/all')
|
||||
->desc('Update all API status')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', [
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
group: 'projects',
|
||||
name: 'updateApiStatusAll',
|
||||
description: '/docs/references/projects/update-api-status-all.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
],
|
||||
deprecated: new Deprecated(
|
||||
since: '1.8.0',
|
||||
replaceWith: 'projects.updateAPIStatusAll',
|
||||
),
|
||||
public: false,
|
||||
),
|
||||
new Method(
|
||||
namespace: 'projects',
|
||||
group: 'projects',
|
||||
name: 'updateAPIStatusAll',
|
||||
description: '/docs/references/projects/update-api-status-all.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
]
|
||||
)
|
||||
])
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('status', null, new Boolean(), 'API status.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, bool $status, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$allApis = array_keys(Config::getParam('apis'));
|
||||
|
||||
$apis = [];
|
||||
foreach ($allApis as $api) {
|
||||
$apis[$api] = $status;
|
||||
}
|
||||
|
||||
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('apis', $apis));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
->action(function () {
|
||||
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED, 'Bulk API no longer exists for services. Please change status individually.');
|
||||
});
|
||||
|
||||
Http::patch('/v1/projects/:projectId/oauth2')
|
||||
@@ -769,288 +577,6 @@ Http::delete('/v1/projects/:projectId')
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
// Keys
|
||||
|
||||
Http::post('/v1/projects/:projectId/keys')
|
||||
->desc('Create key')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'keys.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'keys',
|
||||
name: 'createKey',
|
||||
description: '/docs/references/projects/create-key.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
model: Response::MODEL_KEY,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
// TODO: When migrating to Platform API, mark keyId required for consistency
|
||||
->param('keyId', 'unique()', fn (Database $dbForPlatform) => new CustomId($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key 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.', true, ['dbForPlatform'])->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
|
||||
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
|
||||
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
|
||||
$keyId = $keyId == 'unique()' ? ID::unique() : $keyId;
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$key = new Document([
|
||||
'$id' => $keyId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'resourceInternalId' => $project->getSequence(),
|
||||
'resourceId' => $project->getId(),
|
||||
'resourceType' => 'projects',
|
||||
'name' => $name,
|
||||
'scopes' => $scopes,
|
||||
'expire' => $expire,
|
||||
'sdks' => [],
|
||||
'accessedAt' => null,
|
||||
'secret' => API_KEY_STANDARD . '_' . \bin2hex(\random_bytes(128)),
|
||||
]);
|
||||
|
||||
try {
|
||||
$key = $dbForPlatform->createDocument('keys', $key);
|
||||
} catch (Duplicate) {
|
||||
throw new Exception(Exception::KEY_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($key, Response::MODEL_KEY);
|
||||
});
|
||||
|
||||
Http::get('/v1/projects/:projectId/keys')
|
||||
->desc('List keys')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'keys.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'keys',
|
||||
name: 'listKeys',
|
||||
description: '/docs/references/projects/list-keys.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_KEY_LIST,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('queries', [], new Keys(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Keys::ALLOWED_ATTRIBUTES), true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, array $queries, bool $includeTotal, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
if (\count(Query::getByType($queries, [Query::TYPE_LIMIT])) === 0) {
|
||||
$queries[] = Query::limit(5000);
|
||||
}
|
||||
|
||||
$queries[] = Query::equal('resourceType', ['projects']);
|
||||
$queries[] = Query::equal('resourceInternalId', [$project->getSequence()]);
|
||||
|
||||
$cursor = Query::getCursorQueries($queries, false);
|
||||
$cursor = \reset($cursor);
|
||||
|
||||
if ($cursor !== false) {
|
||||
$validator = new Cursor();
|
||||
if (!$validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$keyId = $cursor->getValue();
|
||||
$cursorDocument = $dbForPlatform->getDocument('keys', $keyId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Key '{$keyId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
$keys = $dbForPlatform->find('keys', $queries);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'keys' => $keys,
|
||||
'total' => $includeTotal ? $dbForPlatform->count('keys', $filterQueries, APP_LIMIT_COUNT) : 0,
|
||||
]), Response::MODEL_KEY_LIST);
|
||||
});
|
||||
|
||||
Http::get('/v1/projects/:projectId/keys/:keyId')
|
||||
->desc('Get key')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'keys.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'keys',
|
||||
name: 'getKey',
|
||||
description: '/docs/references/projects/get-key.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_KEY,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key unique ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $keyId, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$key = $dbForPlatform->findOne('keys', [
|
||||
Query::equal('$id', [$keyId]),
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($key->isEmpty()) {
|
||||
throw new Exception(Exception::KEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($key, Response::MODEL_KEY);
|
||||
});
|
||||
|
||||
Http::put('/v1/projects/:projectId/keys/:keyId')
|
||||
->desc('Update key')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'keys.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'keys',
|
||||
name: 'updateKey',
|
||||
description: '/docs/references/projects/update-key.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_KEY,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key unique ID.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
|
||||
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
|
||||
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$key = $dbForPlatform->findOne('keys', [
|
||||
Query::equal('$id', [$keyId]),
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($key->isEmpty()) {
|
||||
throw new Exception(Exception::KEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$key
|
||||
->setAttribute('name', $name)
|
||||
->setAttribute('scopes', $scopes)
|
||||
->setAttribute('expire', $expire);
|
||||
|
||||
$dbForPlatform->updateDocument('keys', $key->getId(), $key);
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->dynamic($key, Response::MODEL_KEY);
|
||||
});
|
||||
|
||||
Http::delete('/v1/projects/:projectId/keys/:keyId')
|
||||
->desc('Delete key')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'keys.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'keys',
|
||||
name: 'deleteKey',
|
||||
description: '/docs/references/projects/delete-key.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_NOCONTENT,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::NONE
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key unique ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $keyId, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$key = $dbForPlatform->findOne('keys', [
|
||||
Query::equal('$id', [$keyId]),
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($key->isEmpty()) {
|
||||
throw new Exception(Exception::KEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$dbForPlatform->deleteDocument('keys', $key->getId());
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
// JWT Keys
|
||||
|
||||
Http::post('/v1/projects/:projectId/jwts')
|
||||
@@ -1093,272 +619,6 @@ Http::post('/v1/projects/:projectId/jwts')
|
||||
])]), Response::MODEL_JWT);
|
||||
});
|
||||
|
||||
// Platforms
|
||||
|
||||
Http::post('/v1/projects/:projectId/platforms')
|
||||
->desc('Create platform')
|
||||
->groups(['api', 'projects'])
|
||||
->label('audits.event', 'platforms.create')
|
||||
->label('audits.resource', 'project/{request.projectId}')
|
||||
->label('scope', 'platforms.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'platforms',
|
||||
name: 'createPlatform',
|
||||
description: '/docs/references/projects/create-platform.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
model: Response::MODEL_PLATFORM,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param(
|
||||
'type',
|
||||
null,
|
||||
new WhiteList([
|
||||
Platform::TYPE_WEB,
|
||||
Platform::TYPE_FLUTTER_WEB,
|
||||
Platform::TYPE_FLUTTER_IOS,
|
||||
Platform::TYPE_FLUTTER_ANDROID,
|
||||
Platform::TYPE_FLUTTER_LINUX,
|
||||
Platform::TYPE_FLUTTER_MACOS,
|
||||
Platform::TYPE_FLUTTER_WINDOWS,
|
||||
Platform::TYPE_APPLE_IOS,
|
||||
Platform::TYPE_APPLE_MACOS,
|
||||
Platform::TYPE_APPLE_WATCHOS,
|
||||
Platform::TYPE_APPLE_TVOS,
|
||||
Platform::TYPE_ANDROID,
|
||||
Platform::TYPE_UNITY,
|
||||
Platform::TYPE_REACT_NATIVE_IOS,
|
||||
Platform::TYPE_REACT_NATIVE_ANDROID,
|
||||
], true),
|
||||
'Platform type. Possible values are: web, flutter-web, flutter-ios, flutter-android, flutter-linux, flutter-macos, flutter-windows, apple-ios, apple-macos, apple-watchos, apple-tvos, android, unity, react-native-ios, react-native-android.'
|
||||
)
|
||||
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
|
||||
->param('key', '', new Text(256), 'Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', true)
|
||||
->param('store', '', new Text(256), 'App store or Google Play store ID. Max length: 256 chars.', true)
|
||||
->param('hostname', '', new Hostname(), 'Platform client hostname. Max length: 256 chars.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $type, string $name, string $key, string $store, string $hostname, Response $response, Database $dbForPlatform) {
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$platform = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'projectInternalId' => $project->getSequence(),
|
||||
'projectId' => $project->getId(),
|
||||
'type' => $type,
|
||||
'name' => $name,
|
||||
'key' => $key,
|
||||
'store' => $store,
|
||||
'hostname' => $hostname
|
||||
]);
|
||||
|
||||
$platform = $dbForPlatform->createDocument('platforms', $platform);
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($platform, Response::MODEL_PLATFORM);
|
||||
});
|
||||
|
||||
Http::get('/v1/projects/:projectId/platforms')
|
||||
->desc('List platforms')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'platforms.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'platforms',
|
||||
name: 'listPlatforms',
|
||||
description: '/docs/references/projects/list-platforms.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PLATFORM_LIST,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$platforms = $dbForPlatform->find('platforms', [
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
Query::limit(5000),
|
||||
]);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'platforms' => $platforms,
|
||||
'total' => $includeTotal ? count($platforms) : 0,
|
||||
]), Response::MODEL_PLATFORM_LIST);
|
||||
});
|
||||
|
||||
Http::get('/v1/projects/:projectId/platforms/:platformId')
|
||||
->desc('Get platform')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'platforms.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'platforms',
|
||||
name: 'getPlatform',
|
||||
description: '/docs/references/projects/get-platform.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PLATFORM,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform unique ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $platformId, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$platform = $dbForPlatform->findOne('platforms', [
|
||||
Query::equal('$id', [$platformId]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($platform->isEmpty()) {
|
||||
throw new Exception(Exception::PLATFORM_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($platform, Response::MODEL_PLATFORM);
|
||||
});
|
||||
|
||||
Http::put('/v1/projects/:projectId/platforms/:platformId')
|
||||
->desc('Update platform')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'platforms.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'platforms',
|
||||
name: 'updatePlatform',
|
||||
description: '/docs/references/projects/update-platform.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PLATFORM,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform unique ID.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
|
||||
->param('key', '', new Text(256), 'Package name for android or bundle ID for iOS. Max length: 256 chars.', true)
|
||||
->param('store', '', new Text(256), 'App store or Google Play store ID. Max length: 256 chars.', true)
|
||||
->param('hostname', '', new Hostname(), 'Platform client URL. Max length: 256 chars.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $platformId, string $name, string $key, string $store, string $hostname, Response $response, Database $dbForPlatform) {
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$platform = $dbForPlatform->findOne('platforms', [
|
||||
Query::equal('$id', [$platformId]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($platform->isEmpty()) {
|
||||
throw new Exception(Exception::PLATFORM_NOT_FOUND);
|
||||
}
|
||||
|
||||
$platform
|
||||
->setAttribute('name', $name)
|
||||
->setAttribute('key', $key)
|
||||
->setAttribute('store', $store)
|
||||
->setAttribute('hostname', $hostname);
|
||||
|
||||
$dbForPlatform->updateDocument('platforms', $platform->getId(), $platform);
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->dynamic($platform, Response::MODEL_PLATFORM);
|
||||
});
|
||||
|
||||
Http::delete('/v1/projects/:projectId/platforms/:platformId')
|
||||
->desc('Delete platform')
|
||||
->groups(['api', 'projects'])
|
||||
->label('audits.event', 'platforms.delete')
|
||||
->label('audits.resource', 'project/{request.projectId}/platform/${request.platformId}')
|
||||
->label('scope', 'platforms.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'platforms',
|
||||
name: 'deletePlatform',
|
||||
description: '/docs/references/projects/delete-platform.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_NOCONTENT,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::NONE
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform unique ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $platformId, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$platform = $dbForPlatform->findOne('platforms', [
|
||||
Query::equal('$id', [$platformId]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($platform->isEmpty()) {
|
||||
throw new Exception(Exception::PLATFORM_NOT_FOUND);
|
||||
}
|
||||
|
||||
$dbForPlatform->deleteDocument('platforms', $platformId);
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
|
||||
// CUSTOM SMTP and Templates
|
||||
Http::patch('/v1/projects/:projectId/smtp')
|
||||
->desc('Update SMTP')
|
||||
@@ -1570,7 +830,7 @@ Http::post('/v1/projects/:projectId/smtp/tests')
|
||||
->trigger();
|
||||
}
|
||||
|
||||
return $response->noContent();
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
Http::get('/v1/projects/:projectId/templates/sms/:type/:locale')
|
||||
|
||||
+106
-41
@@ -73,7 +73,7 @@ use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
/** TODO: Remove function when we move to using utopia/platform */
|
||||
function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, ?string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
|
||||
function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, ?string $name, Document $project, Database $dbForProject, Hooks $hooks, array $plan): Document
|
||||
{
|
||||
$name = $name ?? '';
|
||||
$plaintextPassword = $password;
|
||||
@@ -110,11 +110,39 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
|
||||
}
|
||||
}
|
||||
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => null,
|
||||
'emailIsCanonical' => null,
|
||||
'emailIsCorporate' => null,
|
||||
'emailIsDisposable' => null,
|
||||
'emailIsFree' => null,
|
||||
];
|
||||
|
||||
try {
|
||||
$emailCanonical = new Email($email);
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
$parsedEmail = new Email($email ?? '');
|
||||
$canonical = $parsedEmail->getCanonical();
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => $canonical,
|
||||
'emailIsCanonical' => $parsedEmail->get() === $canonical,
|
||||
'emailIsCorporate' => $parsedEmail->isCorporate(),
|
||||
'emailIsDisposable' => $parsedEmail->isDisposable(),
|
||||
'emailIsFree' => $parsedEmail->isFree(),
|
||||
];
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
|
||||
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
|
||||
}
|
||||
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
|
||||
}
|
||||
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
|
||||
throw new Exception(Exception::USER_EMAIL_FREE);
|
||||
}
|
||||
|
||||
$hashedPassword = null;
|
||||
|
||||
$isHashed = !$hash instanceof Plaintext;
|
||||
@@ -159,11 +187,11 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
|
||||
'tokens' => null,
|
||||
'memberships' => null,
|
||||
'search' => implode(' ', [$userId, $email, $phone, $name]),
|
||||
'emailCanonical' => $emailCanonical?->getCanonical(),
|
||||
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
|
||||
'emailIsCorporate' => $emailCanonical?->isCorporate(),
|
||||
'emailIsDisposable' => $emailCanonical?->isDisposable(),
|
||||
'emailIsFree' => $emailCanonical?->isFree(),
|
||||
'emailCanonical' => $emailMetadata['emailCanonical'],
|
||||
'emailIsCanonical' => $emailMetadata['emailIsCanonical'],
|
||||
'emailIsCorporate' => $emailMetadata['emailIsCorporate'],
|
||||
'emailIsDisposable' => $emailMetadata['emailIsDisposable'],
|
||||
'emailIsFree' => $emailMetadata['emailIsFree'],
|
||||
]);
|
||||
|
||||
if (!$isHashed && !empty($password)) {
|
||||
@@ -256,10 +284,11 @@ Http::post('/v1/users')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->inject('plan')
|
||||
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
|
||||
$plaintext = new Plaintext();
|
||||
|
||||
$user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks, $plan);
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($user, Response::MODEL_USER);
|
||||
@@ -292,11 +321,12 @@ Http::post('/v1/users/bcrypt')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->inject('plan')
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
|
||||
$bcrypt = new Bcrypt();
|
||||
$bcrypt->setCost(8); // Default cost
|
||||
|
||||
$user = createUser($bcrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser($bcrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -330,10 +360,11 @@ Http::post('/v1/users/md5')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->inject('plan')
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
|
||||
$md5 = new MD5();
|
||||
|
||||
$user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -367,10 +398,11 @@ Http::post('/v1/users/argon2')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->inject('plan')
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
|
||||
$argon2 = new Argon2();
|
||||
|
||||
$user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -405,13 +437,14 @@ Http::post('/v1/users/sha')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordVersion, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->inject('plan')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordVersion, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
|
||||
$sha = new Sha();
|
||||
if (!empty($passwordVersion)) {
|
||||
$sha->setVersion($passwordVersion);
|
||||
}
|
||||
|
||||
$user = createUser($sha, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser($sha, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -445,10 +478,11 @@ Http::post('/v1/users/phpass')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->inject('plan')
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
|
||||
$phpass = new PHPass();
|
||||
|
||||
$user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -487,7 +521,8 @@ Http::post('/v1/users/scrypt')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->inject('plan')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
|
||||
$scrypt = new Scrypt();
|
||||
$scrypt
|
||||
->setSalt($passwordSalt)
|
||||
@@ -496,7 +531,7 @@ Http::post('/v1/users/scrypt')
|
||||
->setParallelCost($passwordParallel)
|
||||
->setLength($passwordLength);
|
||||
|
||||
$user = createUser($scrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser($scrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -533,14 +568,15 @@ Http::post('/v1/users/scrypt-modified')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->inject('plan')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
|
||||
$scryptModified = new ScryptModified();
|
||||
$scryptModified
|
||||
->setSalt($passwordSalt)
|
||||
->setSaltSeparator($passwordSaltSeparator)
|
||||
->setSignerKey($passwordSignerKey);
|
||||
|
||||
$user = createUser($scryptModified, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser($scryptModified, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -972,6 +1008,8 @@ Http::get('/v1/users/:userId/logs')
|
||||
'userId' => ID::custom($log['data']['userId']),
|
||||
'userEmail' => $log['data']['userEmail'] ?? null,
|
||||
'userName' => $log['data']['userName'] ?? null,
|
||||
'mode' => $log['data']['mode'] ?? null,
|
||||
'userType' => $log['data']['userType'] ?? null,
|
||||
'ip' => $log['ip'],
|
||||
'time' => $log['time'],
|
||||
'osCode' => $os['osCode'],
|
||||
@@ -1470,8 +1508,10 @@ Http::patch('/v1/users/:userId/email')
|
||||
->param('email', '', new EmailValidator(allowEmpty: true), 'User email.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('plan')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $email, Response $response, Database $dbForProject, Event $queueForEvents) {
|
||||
->action(function (string $userId, string $email, Response $response, Database $dbForProject, Document $project, array $plan, Event $queueForEvents) {
|
||||
|
||||
$user = $dbForProject->getDocument('users', $userId);
|
||||
|
||||
@@ -1502,20 +1542,47 @@ Http::patch('/v1/users/:userId/email')
|
||||
|
||||
$oldEmail = $user->getAttribute('email');
|
||||
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => null,
|
||||
'emailIsCanonical' => null,
|
||||
'emailIsCorporate' => null,
|
||||
'emailIsDisposable' => null,
|
||||
'emailIsFree' => null,
|
||||
];
|
||||
|
||||
try {
|
||||
$emailCanonical = new Email($email);
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
$parsedEmail = new Email($email);
|
||||
$canonical = $parsedEmail->getCanonical();
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => $canonical,
|
||||
'emailIsCanonical' => $parsedEmail->get() === $canonical,
|
||||
'emailIsCorporate' => $parsedEmail->isCorporate(),
|
||||
'emailIsDisposable' => $parsedEmail->isDisposable(),
|
||||
'emailIsFree' => $parsedEmail->isFree(),
|
||||
];
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
|
||||
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
|
||||
}
|
||||
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
|
||||
}
|
||||
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
|
||||
throw new Exception(Exception::USER_EMAIL_FREE);
|
||||
}
|
||||
|
||||
$user
|
||||
->setAttribute('email', $email)
|
||||
->setAttribute('emailVerification', false)
|
||||
->setAttribute('emailCanonical', $emailCanonical?->getCanonical())
|
||||
->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported())
|
||||
->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate())
|
||||
->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable())
|
||||
->setAttribute('emailIsFree', $emailCanonical?->isFree())
|
||||
->setAttribute('emailCanonical', $emailMetadata['emailCanonical'])
|
||||
->setAttribute('emailIsCanonical', $emailMetadata['emailIsCanonical'])
|
||||
->setAttribute('emailIsCorporate', $emailMetadata['emailIsCorporate'])
|
||||
->setAttribute('emailIsDisposable', $emailMetadata['emailIsDisposable'])
|
||||
->setAttribute('emailIsFree', $emailMetadata['emailIsFree'])
|
||||
;
|
||||
|
||||
try {
|
||||
@@ -2337,9 +2404,8 @@ Http::post('/v1/users/:userId/sessions')
|
||||
->setParam('sessionId', $session->getId())
|
||||
->setPayload($response->output($session, Response::MODEL_SESSION));
|
||||
|
||||
return $response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($session, Response::MODEL_SESSION);
|
||||
$response->setStatusCode(Response::STATUS_CODE_CREATED);
|
||||
$response->dynamic($session, Response::MODEL_SESSION);
|
||||
});
|
||||
|
||||
Http::post('/v1/users/:userId/tokens')
|
||||
@@ -2402,9 +2468,8 @@ Http::post('/v1/users/:userId/tokens')
|
||||
->setParam('tokenId', $token->getId())
|
||||
->setPayload($response->output($token, Response::MODEL_TOKEN));
|
||||
|
||||
return $response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($token, Response::MODEL_TOKEN);
|
||||
$response->setStatusCode(Response::STATUS_CODE_CREATED);
|
||||
$response->dynamic($token, Response::MODEL_TOKEN);
|
||||
});
|
||||
|
||||
Http::delete('/v1/users/:userId/sessions/:sessionId')
|
||||
@@ -2658,7 +2723,7 @@ Http::delete('/v1/users/identities/:identityId')
|
||||
->setParam('identityId', $identity->getId())
|
||||
->setPayload($response->output($identity, Response::MODEL_IDENTITY));
|
||||
|
||||
return $response->noContent();
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
Http::post('/v1/users/:userId/jwts')
|
||||
|
||||
+47
-56
@@ -25,6 +25,7 @@ use Appwrite\Utopia\Request\Filters\V18 as RequestV18;
|
||||
use Appwrite\Utopia\Request\Filters\V19 as RequestV19;
|
||||
use Appwrite\Utopia\Request\Filters\V20 as RequestV20;
|
||||
use Appwrite\Utopia\Request\Filters\V21 as RequestV21;
|
||||
use Appwrite\Utopia\Request\Filters\V22 as RequestV22;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
|
||||
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
|
||||
@@ -32,6 +33,7 @@ use Appwrite\Utopia\Response\Filters\V18 as ResponseV18;
|
||||
use Appwrite\Utopia\Response\Filters\V19 as ResponseV19;
|
||||
use Appwrite\Utopia\Response\Filters\V20 as ResponseV20;
|
||||
use Appwrite\Utopia\Response\Filters\V21 as ResponseV21;
|
||||
use Appwrite\Utopia\Response\Filters\V22 as ResponseV22;
|
||||
use Appwrite\Utopia\View;
|
||||
use Executor\Executor;
|
||||
use MaxMind\Db\Reader;
|
||||
@@ -61,8 +63,6 @@ use Utopia\System\System;
|
||||
use Utopia\Validator;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
Config::setParam('domainVerification', false);
|
||||
Config::setParam('cookieDomain', 'localhost');
|
||||
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
|
||||
|
||||
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
|
||||
@@ -166,14 +166,14 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
if ($request->getMethod() !== Request::METHOD_GET) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.', view: $errorView);
|
||||
}
|
||||
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
|
||||
$response->redirect('https://' . $request->getHostname() . $request->getURI());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var Database $dbForProject */
|
||||
$dbForProject = $getProjectDB($project);
|
||||
|
||||
/** @var Document $deployment */
|
||||
if (!empty($rule->getAttribute('deploymentId', ''))) {
|
||||
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId')));
|
||||
} else {
|
||||
@@ -244,6 +244,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
if ($isPreview && $requirePreview) {
|
||||
$cookie = $request->getCookie(COOKIE_NAME_PREVIEW, '');
|
||||
$authorized = false;
|
||||
$user = new Document();
|
||||
|
||||
// Security checks to mark authorized true
|
||||
if (!empty($cookie)) {
|
||||
@@ -273,7 +274,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
|
||||
$membershipExists = false;
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
|
||||
if (!$project->isEmpty() && isset($user)) {
|
||||
if (!$project->isEmpty() && !$user->isEmpty()) {
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
$membership = $user->find('teamId', $teamId, 'memberships');
|
||||
if (!empty($membership)) {
|
||||
@@ -379,7 +380,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
$executionId = ID::unique();
|
||||
|
||||
$headers = \array_merge([], $requestHeaders);
|
||||
$headers['x-appwrite-execution-id'] = $executionId ?? '';
|
||||
$headers['x-appwrite-execution-id'] = $executionId;
|
||||
$headers['x-appwrite-user-id'] = '';
|
||||
$headers['x-appwrite-country-code'] = '';
|
||||
$headers['x-appwrite-continent-code'] = '';
|
||||
@@ -459,7 +460,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
if ($version === 'v2') {
|
||||
$vars = \array_merge($vars, [
|
||||
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
|
||||
'APPWRITE_FUNCTION_DATA' => $body ?? '',
|
||||
'APPWRITE_FUNCTION_DATA' => $body,
|
||||
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
|
||||
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
|
||||
]);
|
||||
@@ -529,6 +530,11 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
}
|
||||
|
||||
/** Execute function */
|
||||
$executionResponse = [
|
||||
'headers' => [],
|
||||
'body' => '',
|
||||
];
|
||||
|
||||
try {
|
||||
$version = match ($type) {
|
||||
'function' => $resource->getAttribute('version', 'v2'),
|
||||
@@ -734,7 +740,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
$execution->setAttribute('responseBody', $executionResponse['body'] ?? '');
|
||||
$execution->setAttribute('responseHeaders', $headers);
|
||||
|
||||
$body = $execution['responseBody'] ?? '';
|
||||
$body = $execution['responseBody'];
|
||||
|
||||
$contentType = 'text/plain';
|
||||
foreach ($executionResponse['headers'] as $name => $values) {
|
||||
@@ -748,11 +754,8 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
}
|
||||
|
||||
if (\is_array($values)) {
|
||||
$count = 0;
|
||||
foreach ($values as $value) {
|
||||
$override = $count === 0;
|
||||
$response->addHeader($name, $value, override: $override);
|
||||
$count++;
|
||||
$response->addHeader($name, $value);
|
||||
}
|
||||
} else {
|
||||
$response->addHeader($name, $values);
|
||||
@@ -862,12 +865,12 @@ Http::init()
|
||||
* Request format
|
||||
*/
|
||||
$route = $utopia->getRoute();
|
||||
Request::setRoute($route);
|
||||
$request->setRoute($route);
|
||||
|
||||
if ($route === null) {
|
||||
return $response
|
||||
->setStatusCode(404)
|
||||
->send('Not Found');
|
||||
$response->setStatusCode(404);
|
||||
$response->send('Not Found');
|
||||
return;
|
||||
}
|
||||
|
||||
$requestFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
|
||||
@@ -891,6 +894,9 @@ Http::init()
|
||||
if (version_compare($requestFormat, '1.9.0', '<')) {
|
||||
$request->addFilter(new RequestV21());
|
||||
}
|
||||
if (version_compare($requestFormat, '1.9.1', '<')) {
|
||||
$request->addFilter(new RequestV22());
|
||||
}
|
||||
}
|
||||
|
||||
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
|
||||
@@ -898,40 +904,16 @@ Http::init()
|
||||
$locale->setDefault($localeParam);
|
||||
}
|
||||
|
||||
$origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST);
|
||||
$selfDomain = new Domain($request->getHostname());
|
||||
$endDomain = new Domain((string)$origin);
|
||||
Config::setParam(
|
||||
'domainVerification',
|
||||
($selfDomain->getRegisterable() === $endDomain->getRegisterable()) &&
|
||||
$endDomain->getRegisterable() !== ''
|
||||
);
|
||||
|
||||
$localHosts = ['localhost','localhost:'.$request->getPort()];
|
||||
|
||||
$migrationHost = System::getEnv('_APP_MIGRATION_HOST');
|
||||
if (!empty($migrationHost)) {
|
||||
// Treat the migration host like localhost because internal migration and
|
||||
// CI traffic may use it before a public domain is configured.
|
||||
$localHosts[] = $migrationHost;
|
||||
$localHosts[] = $migrationHost.':'.$request->getPort();
|
||||
}
|
||||
|
||||
$isLocalHost = in_array($request->getHostname(), $localHosts);
|
||||
$isIpAddress = filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false;
|
||||
|
||||
$isConsoleProject = $project->getAttribute('$id', '') === 'console';
|
||||
$isConsoleRootSession = System::getEnv('_APP_CONSOLE_ROOT_SESSION', 'disabled') === 'enabled';
|
||||
|
||||
Config::setParam(
|
||||
'cookieDomain',
|
||||
$isLocalHost || $isIpAddress
|
||||
? null
|
||||
: (
|
||||
$isConsoleProject && $isConsoleRootSession
|
||||
? '.' . $selfDomain->getRegisterable()
|
||||
: '.' . $request->getHostname()
|
||||
)
|
||||
);
|
||||
|
||||
$warnings = [];
|
||||
|
||||
/*
|
||||
@@ -939,6 +921,9 @@ Http::init()
|
||||
*/
|
||||
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
|
||||
if ($responseFormat) {
|
||||
if (version_compare($responseFormat, '1.9.1', '<')) {
|
||||
$response->addFilter(new ResponseV22());
|
||||
}
|
||||
if (version_compare($responseFormat, '1.9.0', '<')) {
|
||||
$response->addFilter(new ResponseV21());
|
||||
}
|
||||
@@ -973,7 +958,8 @@ Http::init()
|
||||
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.');
|
||||
}
|
||||
|
||||
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
|
||||
$response->redirect('https://' . $request->getHostname() . $request->getURI());
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1012,7 +998,7 @@ Http::init()
|
||||
return;
|
||||
}
|
||||
$route = $request->getRoute();
|
||||
if ($route->getLabel('origin', false) === '*') {
|
||||
if ($route?->getLabel('origin', false) === '*') {
|
||||
return;
|
||||
}
|
||||
if (!$originValidator->isValid($origin)) {
|
||||
@@ -1190,15 +1176,6 @@ Http::error()
|
||||
->inject('devKey')
|
||||
->inject('authorization')
|
||||
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) {
|
||||
$trace = $error->getTrace();
|
||||
|
||||
foreach (array_slice($trace, 0, 100) as $index => $traceEntry) {
|
||||
$file = isset($traceEntry['file']) ? $traceEntry['file'] : '[internal function]';
|
||||
$line = isset($traceEntry['line']) ? $traceEntry['line'] : '';
|
||||
$function = isset($traceEntry['function']) ? $traceEntry['function'] : '';
|
||||
Console::error("[$index] $file : $line -> $function()");
|
||||
}
|
||||
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
$route = $utopia->getRoute();
|
||||
$class = \get_class($error);
|
||||
@@ -1208,9 +1185,7 @@ Http::error()
|
||||
$line = $error->getLine();
|
||||
$trace = $error->getTrace();
|
||||
|
||||
if (php_sapi_name() === 'cli') {
|
||||
Span::error($error);
|
||||
}
|
||||
Span::error($error);
|
||||
|
||||
switch ($class) {
|
||||
case Utopia\Http\Exception::class:
|
||||
@@ -1452,6 +1427,7 @@ Http::error()
|
||||
case 402: // Error allowed publicly
|
||||
case 403: // Error allowed publicly
|
||||
case 404: // Error allowed publicly
|
||||
case 405: // Error allowed publicly
|
||||
case 408: // Error allowed publicly
|
||||
case 409: // Error allowed publicly
|
||||
case 412: // Error allowed publicly
|
||||
@@ -1486,6 +1462,21 @@ Http::error()
|
||||
'type' => $type,
|
||||
];
|
||||
|
||||
// Add CORS headers to error responses so browsers can read the error.
|
||||
// Wrapped in try-catch: if the error itself is a DB failure, resolving
|
||||
// the cors resource (which depends on rule -> DB) would cascade.
|
||||
// Uses override:true to avoid duplicate headers if init() already set them.
|
||||
try {
|
||||
$cors = $utopia->getResource('cors');
|
||||
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
|
||||
$response
|
||||
->removeHeader($name)
|
||||
->addHeader($name, $value);
|
||||
}
|
||||
} catch (Throwable) {
|
||||
// Degrade gracefully - error response without CORS is no worse than before.
|
||||
}
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
->addHeader('Expires', '0')
|
||||
|
||||
+30
-28
@@ -244,36 +244,38 @@ Http::get('/v1/mock/github/callback')
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND, $error);
|
||||
}
|
||||
|
||||
if (!empty($providerInstallationId)) {
|
||||
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
|
||||
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
|
||||
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
|
||||
$owner = $github->getOwnerName($providerInstallationId) ?? '';
|
||||
|
||||
$projectInternalId = $project->getSequence();
|
||||
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
|
||||
$installation = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
Permission::read(Role::team(ID::custom($teamId))),
|
||||
Permission::update(Role::team(ID::custom($teamId), 'owner')),
|
||||
Permission::update(Role::team(ID::custom($teamId), 'developer')),
|
||||
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
|
||||
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
|
||||
],
|
||||
'providerInstallationId' => $providerInstallationId,
|
||||
'projectId' => $projectId,
|
||||
'projectInternalId' => $projectInternalId,
|
||||
'provider' => 'github',
|
||||
'organization' => $owner,
|
||||
'personal' => false
|
||||
]);
|
||||
|
||||
$installation = $dbForPlatform->createDocument('installations', $installation);
|
||||
if (empty($providerInstallationId)) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Missing provider installation ID');
|
||||
}
|
||||
|
||||
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
|
||||
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
|
||||
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
|
||||
$owner = $github->getOwnerName($providerInstallationId) ?? '';
|
||||
|
||||
$projectInternalId = $project->getSequence();
|
||||
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
|
||||
$installation = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
Permission::read(Role::team(ID::custom($teamId))),
|
||||
Permission::update(Role::team(ID::custom($teamId), 'owner')),
|
||||
Permission::update(Role::team(ID::custom($teamId), 'developer')),
|
||||
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
|
||||
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
|
||||
],
|
||||
'providerInstallationId' => $providerInstallationId,
|
||||
'projectId' => $projectId,
|
||||
'projectInternalId' => $projectInternalId,
|
||||
'provider' => 'github',
|
||||
'organization' => $owner,
|
||||
'personal' => false
|
||||
]);
|
||||
|
||||
$installation = $dbForPlatform->createDocument('installations', $installation);
|
||||
|
||||
$response->json([
|
||||
'installationId' => $installation->getId(),
|
||||
]);
|
||||
|
||||
@@ -37,6 +37,7 @@ use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Authorization\Input;
|
||||
use Utopia\Database\Validator\Roles;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Span\Span;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter as Telemetry;
|
||||
use Utopia\Validator\WhiteList;
|
||||
@@ -98,6 +99,9 @@ Http::init()
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
|
||||
$route = $utopia->getRoute();
|
||||
if ($route === null) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user authentication and session validation.
|
||||
@@ -183,7 +187,7 @@ Http::init()
|
||||
$user = new User([
|
||||
'$id' => '',
|
||||
'status' => true,
|
||||
'type' => ACTIVITY_TYPE_APP,
|
||||
'type' => ACTIVITY_TYPE_KEY_PROJECT,
|
||||
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
|
||||
'password' => '',
|
||||
'name' => $apiKey->getName(),
|
||||
@@ -253,7 +257,14 @@ Http::init()
|
||||
}
|
||||
}
|
||||
|
||||
$queueForAudits->setUser($user);
|
||||
$userClone = clone $user;
|
||||
$userClone->setAttribute('type', match ($apiKey->getType()) {
|
||||
API_KEY_STANDARD => ACTIVITY_TYPE_KEY_PROJECT,
|
||||
API_KEY_ACCOUNT => ACTIVITY_TYPE_KEY_ACCOUNT,
|
||||
API_KEY_ORGANIZATION => ACTIVITY_TYPE_KEY_ORGANIZATION,
|
||||
default => ACTIVITY_TYPE_KEY_PROJECT,
|
||||
});
|
||||
$queueForAudits->setUser($userClone);
|
||||
}
|
||||
|
||||
// Apply permission
|
||||
@@ -414,7 +425,7 @@ Http::init()
|
||||
}
|
||||
|
||||
if (! empty($method)) {
|
||||
$namespace = $method->getNamespace();
|
||||
$namespace = \strtolower($method->getNamespace());
|
||||
|
||||
if (
|
||||
array_key_exists($namespace, $project->getAttribute('services', []))
|
||||
@@ -425,6 +436,15 @@ Http::init()
|
||||
}
|
||||
}
|
||||
|
||||
// Step 8b: Check REST protocol status
|
||||
if (
|
||||
array_key_exists('rest', $project->getAttribute('apis', []))
|
||||
&& ! $project->getAttribute('apis', [])['rest']
|
||||
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
|
||||
// Step 9: Validate scope permissions
|
||||
$allowed = (array) $route->getLabel('scope', 'none');
|
||||
if (empty(\array_intersect($allowed, $scopes))) {
|
||||
@@ -489,6 +509,10 @@ Http::init()
|
||||
$request->setUser($user);
|
||||
|
||||
$route = $utopia->getRoute();
|
||||
if ($route === null) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$path = $route->getMatchedPath();
|
||||
$databaseType = match (true) {
|
||||
str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
|
||||
@@ -496,14 +520,6 @@ Http::init()
|
||||
default => '',
|
||||
};
|
||||
|
||||
if (
|
||||
array_key_exists('rest', $project->getAttribute('apis', []))
|
||||
&& ! $project->getAttribute('apis', [])['rest']
|
||||
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
|
||||
/*
|
||||
* Abuse Check
|
||||
*/
|
||||
@@ -592,7 +608,9 @@ Http::init()
|
||||
if (! $user->isEmpty()) {
|
||||
$userClone = clone $user;
|
||||
// $user doesn't support `type` and can cause unintended effects.
|
||||
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
|
||||
if (empty($user->getAttribute('type'))) {
|
||||
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
|
||||
}
|
||||
$queueForAudits->setUser($userClone);
|
||||
}
|
||||
|
||||
@@ -617,6 +635,7 @@ Http::init()
|
||||
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$key = $request->cacheIdentifier();
|
||||
Span::add('storage.cache.key', $key);
|
||||
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
$cache = new Cache(
|
||||
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
|
||||
@@ -665,6 +684,8 @@ Http::init()
|
||||
if ($file->isEmpty()) {
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
Span::add('storage.bucket.id', $bucketId);
|
||||
Span::add('storage.file.id', $fileId);
|
||||
// Do not update transformedAt if it's a console user
|
||||
if (! $user->isPrivileged($authorization->getRoles())) {
|
||||
$transformedAt = $file->getAttribute('transformedAt', '');
|
||||
@@ -677,16 +698,27 @@ Http::init()
|
||||
}
|
||||
}
|
||||
|
||||
$accessedAt = $cacheLog->getAttribute('accessedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), new Document([
|
||||
'accessedAt' => DateTime::now(),
|
||||
])));
|
||||
// Refresh the filesystem file's mtime so TTL-based expiry in cache->load() stays valid
|
||||
$cache->save($key, $data);
|
||||
}
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', sprintf('private, max-age=%d', $timestamp))
|
||||
->addHeader('X-Appwrite-Cache', 'hit')
|
||||
->setContentType($cacheLog->getAttribute('mimeType'));
|
||||
$storageCacheOperationsCounter->add(1, ['result' => 'hit']);
|
||||
if (! $isImageTransformation || ! $isDisabled) {
|
||||
Span::add('storage.cache.hit', true);
|
||||
$response->send($data);
|
||||
}
|
||||
} else {
|
||||
$storageCacheOperationsCounter->add(1, ['result' => 'miss']);
|
||||
Span::add('storage.cache.hit', false);
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
@@ -774,7 +806,8 @@ Http::shutdown()
|
||||
->inject('eventProcessor')
|
||||
->inject('bus')
|
||||
->inject('apiKey')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey) use ($parseLabel) {
|
||||
->inject('mode')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
|
||||
|
||||
$responsePayload = $response->getPayload();
|
||||
|
||||
@@ -876,7 +909,9 @@ Http::shutdown()
|
||||
if (! $user->isEmpty()) {
|
||||
$userClone = clone $user;
|
||||
// $user doesn't support `type` and can cause unintended effects.
|
||||
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
|
||||
if (empty($user->getAttribute('type'))) {
|
||||
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
|
||||
}
|
||||
$queueForAudits->setUser($userClone);
|
||||
} elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) {
|
||||
/**
|
||||
|
||||
+59
-55
@@ -1,14 +1,13 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
require_once __DIR__ . '/init.php';
|
||||
require_once __DIR__ . '/init/span.php';
|
||||
|
||||
$registerRequestResources = require __DIR__ . '/init/resources/request.php';
|
||||
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Swoole\Constant;
|
||||
use Swoole\Http\Request as SwooleRequest;
|
||||
use Swoole\Http\Response as SwooleResponse;
|
||||
use Swoole\Http\Server;
|
||||
use Swoole\Process;
|
||||
use Swoole\Table;
|
||||
use Swoole\Timer;
|
||||
@@ -27,11 +26,11 @@ use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Http\Adapter\Swoole\Server;
|
||||
use Utopia\Http\Files;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Logger\Log\User;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Span\Span;
|
||||
use Utopia\System\System;
|
||||
|
||||
@@ -48,18 +47,35 @@ $certifiedDomains = new Table(100_000);
|
||||
$certifiedDomains->column('value', Table::TYPE_INT, 1);
|
||||
$certifiedDomains->create();
|
||||
|
||||
Http::setResource('riskyDomains', fn () => $riskyDomains);
|
||||
Http::setResource('certifiedDomains', fn () => $certifiedDomains);
|
||||
|
||||
$http = new Server(
|
||||
host: "0.0.0.0",
|
||||
port: System::getEnv('PORT', 80),
|
||||
mode: SWOOLE_PROCESS,
|
||||
);
|
||||
global $container;
|
||||
$container->set('riskyDomains', fn () => $riskyDomains);
|
||||
$container->set('certifiedDomains', fn () => $certifiedDomains);
|
||||
$container->set('pools', function ($register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
|
||||
$payloadSize = 12 * (1024 * 1024); // 12MB - adding slight buffer for headers and other data that might be sent with the payload - update later with valid testing
|
||||
$totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
|
||||
|
||||
$swooleAdapter = new Server(
|
||||
host: "0.0.0.0",
|
||||
port: System::getEnv('PORT', 80),
|
||||
settings: [
|
||||
Constant::OPTION_WORKER_NUM => $totalWorkers,
|
||||
Constant::OPTION_DISPATCH_FUNC => dispatch(...),
|
||||
Constant::OPTION_DISPATCH_MODE => SWOOLE_DISPATCH_UIDMOD,
|
||||
Constant::OPTION_HTTP_COMPRESSION => false,
|
||||
Constant::OPTION_PACKAGE_MAX_LENGTH => $payloadSize,
|
||||
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
|
||||
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
|
||||
],
|
||||
container: $container,
|
||||
);
|
||||
|
||||
$container->set('container', fn () => fn () => $swooleAdapter->getContainer());
|
||||
|
||||
$http = $swooleAdapter->getServer();
|
||||
|
||||
/**
|
||||
* Assigns HTTP requests to worker threads by analyzing its payload/content.
|
||||
*
|
||||
@@ -68,16 +84,16 @@ $totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intva
|
||||
* riskier tasks to a dedicated worker subset. Prefers idle workers, with fallback to random selection if necessary.
|
||||
* doc: https://openswoole.com/docs/modules/swoole-server/configuration#dispatch_func
|
||||
*
|
||||
* @param Server $server Swoole server instance.
|
||||
* @param \Swoole\Http\Server $server Swoole server instance.
|
||||
* @param int $fd client ID
|
||||
* @param int $type the type of data and its current state
|
||||
* @param string|null $data Request content for categorization.
|
||||
* @global int $totalThreads Total number of workers.
|
||||
* @return int Chosen worker ID for the request.
|
||||
*/
|
||||
function dispatch(Server $server, int $fd, int $type, $data = null): int
|
||||
function dispatch(\Swoole\Http\Server $server, int $fd, int $type, $data = null): int
|
||||
{
|
||||
$resolveWorkerId = function (Server $server, $data = null) {
|
||||
$resolveWorkerId = function (\Swoole\Http\Server $server, $data = null) {
|
||||
global $totalWorkers, $riskyDomains;
|
||||
|
||||
// If data is not set we can send request to any worker
|
||||
@@ -160,18 +176,6 @@ function dispatch(Server $server, int $fd, int $type, $data = null): int
|
||||
return $workerId;
|
||||
}
|
||||
|
||||
|
||||
$http
|
||||
->set([
|
||||
Constant::OPTION_WORKER_NUM => $totalWorkers,
|
||||
Constant::OPTION_DISPATCH_FUNC => dispatch(...),
|
||||
Constant::OPTION_DISPATCH_MODE => SWOOLE_DISPATCH_UIDMOD,
|
||||
Constant::OPTION_HTTP_COMPRESSION => false,
|
||||
Constant::OPTION_PACKAGE_MAX_LENGTH => $payloadSize,
|
||||
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
|
||||
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
|
||||
]);
|
||||
|
||||
$http->on(Constant::EVENT_WORKER_START, function ($server, $workerId) {
|
||||
});
|
||||
|
||||
@@ -188,16 +192,14 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
|
||||
Console::success('Reload completed...');
|
||||
});
|
||||
|
||||
Http::setResource('bus', function ($register, $utopia) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $utopia->getResource($name));
|
||||
}, ['register', 'utopia']);
|
||||
$container->set('bus', function ($register) use ($swooleAdapter) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $swooleAdapter->getContainer()->get($name));
|
||||
}, ['register']);
|
||||
|
||||
include __DIR__ . '/controllers/general.php';
|
||||
|
||||
function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
|
||||
{
|
||||
$max = 15;
|
||||
$sleep = 2;
|
||||
$max = 15;
|
||||
$sleep = 2;
|
||||
$attempts = 0;
|
||||
@@ -288,13 +290,13 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c
|
||||
Span::current()?->finish();
|
||||
}
|
||||
|
||||
$http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $totalWorkers, $register) {
|
||||
$app = new Http('UTC');
|
||||
$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $swooleAdapter) {
|
||||
$app = new Http($swooleAdapter, 'UTC');
|
||||
|
||||
go(function () use ($register, $app) {
|
||||
$pools = $register->get('pools');
|
||||
/** @var Group $pools */
|
||||
Http::setResource('pools', fn () => $pools);
|
||||
/** @var \Utopia\Pools\Group $pools */
|
||||
$pools = $app->getResource('pools');
|
||||
|
||||
go(function () use ($app, $pools) {
|
||||
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
@@ -510,14 +512,11 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $tot
|
||||
});
|
||||
});
|
||||
|
||||
$http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register, $files) {
|
||||
$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swooleAdapter, $registerRequestResources) {
|
||||
Span::init('http.request');
|
||||
|
||||
Http::setResource('swooleRequest', fn () => $swooleRequest);
|
||||
Http::setResource('swooleResponse', fn () => $swooleResponse);
|
||||
|
||||
$request = new Request($swooleRequest);
|
||||
$response = new Response($swooleResponse);
|
||||
$request = new Request($utopiaRequest->getSwooleRequest());
|
||||
$response = new Response($utopiaResponse->getSwooleResponse());
|
||||
|
||||
Span::add('http.method', $request->getMethod());
|
||||
|
||||
@@ -533,13 +532,18 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
|
||||
return;
|
||||
}
|
||||
|
||||
$app = new Http('UTC');
|
||||
$requestContainer = $swooleAdapter->getContainer();
|
||||
$requestContainer->set('request', fn () => $request);
|
||||
$requestContainer->set('response', fn () => $response);
|
||||
|
||||
$app = new Http($swooleAdapter, 'UTC');
|
||||
$requestContainer->set('utopia', fn () => $app);
|
||||
|
||||
$registerRequestResources($requestContainer);
|
||||
|
||||
$app->setCompression(System::getEnv('_APP_COMPRESSION_ENABLED', 'enabled') === 'enabled');
|
||||
$app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB
|
||||
|
||||
$pools = $register->get('pools');
|
||||
Http::setResource('pools', fn () => $pools);
|
||||
|
||||
try {
|
||||
$authorization = $app->getResource('authorization');
|
||||
|
||||
@@ -623,6 +627,7 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
|
||||
}
|
||||
}
|
||||
|
||||
$swooleResponse = $utopiaResponse->getSwooleResponse();
|
||||
$swooleResponse->setStatusCode(500);
|
||||
|
||||
$output = ((Http::isDevelopment())) ? [
|
||||
@@ -646,16 +651,15 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
|
||||
});
|
||||
|
||||
// Fetch domains every `DOMAIN_SYNC_TIMER` seconds and update in the memory
|
||||
$http->on(Constant::EVENT_TASK, function () use ($register) {
|
||||
$http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
|
||||
$lastSyncUpdate = null;
|
||||
$pools = $register->get('pools');
|
||||
Http::setResource('pools', fn () => $pools);
|
||||
$app = new Http('UTC');
|
||||
|
||||
$app = new Http($swooleAdapter, 'UTC');
|
||||
|
||||
/** @var Utopia\Database\Database $dbForPlatform */
|
||||
$dbForPlatform = $app->getResource('dbForPlatform');
|
||||
|
||||
/** @var Table $riskyDomains */
|
||||
/** @var \Swoole\Table $riskyDomains */
|
||||
$riskyDomains = $app->getResource('riskyDomains');
|
||||
|
||||
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $app) {
|
||||
@@ -725,4 +729,4 @@ $http->on(Constant::EVENT_TASK, function () use ($register) {
|
||||
});
|
||||
});
|
||||
|
||||
$http->start();
|
||||
$swooleAdapter->start();
|
||||
|
||||
@@ -12,7 +12,7 @@ Config::load('runtimes-v2', __DIR__ . '/../config/runtimes-v2.php', $configAdapt
|
||||
Config::load('template-runtimes', __DIR__ . '/../config/template-runtimes.php', $configAdapter);
|
||||
Config::load('events', __DIR__ . '/../config/events.php', $configAdapter);
|
||||
Config::load('auth', __DIR__ . '/../config/auth.php', $configAdapter);
|
||||
Config::load('apis', __DIR__ . '/../config/apis.php', $configAdapter); // List of APIs
|
||||
Config::load('protocols', __DIR__ . '/../config/protocols.php', $configAdapter);
|
||||
Config::load('errors', __DIR__ . '/../config/errors.php', $configAdapter);
|
||||
Config::load('oAuthProviders', __DIR__ . '/../config/oAuthProviders.php', $configAdapter);
|
||||
Config::load('sdks', __DIR__ . '/../config/sdks.php', $configAdapter);
|
||||
|
||||
@@ -46,8 +46,8 @@ const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_BUSTER = 4321;
|
||||
const APP_VERSION_STABLE = '1.9.0';
|
||||
const APP_CACHE_BUSTER = 4322;
|
||||
const APP_VERSION_STABLE = '1.9.1';
|
||||
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
|
||||
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
|
||||
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
|
||||
@@ -156,9 +156,12 @@ const SESSION_PROVIDER_SERVER = 'server';
|
||||
/**
|
||||
* Activity associated with user or the app.
|
||||
*/
|
||||
const ACTIVITY_TYPE_APP = 'app';
|
||||
const ACTIVITY_TYPE_USER = 'user';
|
||||
const ACTIVITY_TYPE_ADMIN = 'admin';
|
||||
const ACTIVITY_TYPE_GUEST = 'guest';
|
||||
const ACTIVITY_TYPE_KEY_PROJECT = 'keyProject';
|
||||
const ACTIVITY_TYPE_KEY_ACCOUNT = 'keyAccount';
|
||||
const ACTIVITY_TYPE_KEY_ORGANIZATION = 'keyOrganization';
|
||||
|
||||
/**
|
||||
* MFA
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\OpenSSL\OpenSSL;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
@@ -123,11 +124,17 @@ Database::addFilter(
|
||||
return;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
return $database->getAuthorization()->skip(fn () => $database
|
||||
$platforms = $database->getAuthorization()->skip(fn () => $database
|
||||
->find('platforms', [
|
||||
Query::equal('projectInternalId', [$document->getSequence()]),
|
||||
Query::limit(APP_LIMIT_SUBQUERY),
|
||||
]));
|
||||
|
||||
foreach ($platforms as $platform) {
|
||||
$platform->setAttribute('type', Platform::mapDeprecatedType($platform->getAttribute('type')));
|
||||
}
|
||||
|
||||
return $platforms;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
+12
-3
@@ -106,7 +106,12 @@ use Appwrite\Utopia\Response\Model\Mock;
|
||||
use Appwrite\Utopia\Response\Model\MockNumber;
|
||||
use Appwrite\Utopia\Response\Model\None;
|
||||
use Appwrite\Utopia\Response\Model\Phone;
|
||||
use Appwrite\Utopia\Response\Model\Platform;
|
||||
use Appwrite\Utopia\Response\Model\PlatformAndroid;
|
||||
use Appwrite\Utopia\Response\Model\PlatformApple;
|
||||
use Appwrite\Utopia\Response\Model\PlatformLinux;
|
||||
use Appwrite\Utopia\Response\Model\PlatformList;
|
||||
use Appwrite\Utopia\Response\Model\PlatformWeb;
|
||||
use Appwrite\Utopia\Response\Model\PlatformWindows;
|
||||
use Appwrite\Utopia\Response\Model\Preferences;
|
||||
use Appwrite\Utopia\Response\Model\Project;
|
||||
use Appwrite\Utopia\Response\Model\Provider;
|
||||
@@ -197,7 +202,6 @@ Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, '
|
||||
Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, true));
|
||||
Response::setModel(new BaseList('Dev Keys List', Response::MODEL_DEV_KEY_LIST, 'devKeys', Response::MODEL_DEV_KEY, true, false));
|
||||
Response::setModel(new BaseList('Auth Providers List', Response::MODEL_AUTH_PROVIDER_LIST, 'platforms', Response::MODEL_AUTH_PROVIDER, true, false));
|
||||
Response::setModel(new BaseList('Platforms List', Response::MODEL_PLATFORM_LIST, 'platforms', Response::MODEL_PLATFORM, true, false));
|
||||
Response::setModel(new BaseList('Countries List', Response::MODEL_COUNTRY_LIST, 'countries', Response::MODEL_COUNTRY));
|
||||
Response::setModel(new BaseList('Continents List', Response::MODEL_CONTINENT_LIST, 'continents', Response::MODEL_CONTINENT));
|
||||
Response::setModel(new BaseList('Languages List', Response::MODEL_LANGUAGE_LIST, 'languages', Response::MODEL_LANGUAGE));
|
||||
@@ -333,7 +337,12 @@ Response::setModel(new Key());
|
||||
Response::setModel(new DevKey());
|
||||
Response::setModel(new MockNumber());
|
||||
Response::setModel(new AuthProvider());
|
||||
Response::setModel(new Platform());
|
||||
Response::setModel(new PlatformWeb());
|
||||
Response::setModel(new PlatformApple());
|
||||
Response::setModel(new PlatformAndroid());
|
||||
Response::setModel(new PlatformWindows());
|
||||
Response::setModel(new PlatformLinux());
|
||||
Response::setModel(new PlatformList());
|
||||
Response::setModel(new Variable());
|
||||
Response::setModel(new Country());
|
||||
Response::setModel(new Continent());
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
<?php
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Utopia\Auth\Hashes\Sha;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Auth\Store;
|
||||
use Utopia\Database\DateTime as DatabaseDateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DI\Container;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\URL;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
/**
|
||||
* Register the minimal per-connection resources required by realtime.
|
||||
*/
|
||||
return function (Container $container): void {
|
||||
$getProjectId = static function (Request $request): string {
|
||||
$projectId = $request->getHeader('x-appwrite-project', '');
|
||||
|
||||
if (!empty($projectId)) {
|
||||
return $projectId;
|
||||
}
|
||||
|
||||
$projectId = $request->getParam('project', '');
|
||||
|
||||
return \is_string($projectId) ? $projectId : '';
|
||||
};
|
||||
|
||||
$getMode = static function (Request $request, Document $project) use ($getProjectId): string {
|
||||
$mode = $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT));
|
||||
$projectId = $getProjectId($request);
|
||||
|
||||
if (!empty($projectId) && $project->getId() !== $projectId) {
|
||||
$mode = APP_MODE_ADMIN;
|
||||
}
|
||||
|
||||
return $mode;
|
||||
};
|
||||
|
||||
$getDbForPlatform = static function (Authorization $authorization) {
|
||||
$database = getConsoleDB();
|
||||
$database->setAuthorization($authorization);
|
||||
|
||||
return $database;
|
||||
};
|
||||
|
||||
$getDbForProject = static function (Document $project, Authorization $authorization) use ($getDbForPlatform) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $getDbForPlatform($authorization);
|
||||
}
|
||||
|
||||
$database = getProjectDB($project);
|
||||
$database->setAuthorization($authorization);
|
||||
|
||||
return $database;
|
||||
};
|
||||
|
||||
$findRule = static function (Request $request, Document $project, Authorization $authorization) use ($getDbForPlatform): Document {
|
||||
$domain = \parse_url($request->getOrigin(), PHP_URL_HOST);
|
||||
|
||||
if (empty($domain)) {
|
||||
$domain = \parse_url($request->getReferer(), PHP_URL_HOST);
|
||||
}
|
||||
|
||||
if (empty($domain)) {
|
||||
return new Document();
|
||||
}
|
||||
|
||||
$dbForPlatform = $getDbForPlatform($authorization);
|
||||
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
|
||||
|
||||
$rule = $authorization->skip(function () use ($dbForPlatform, $domain, $isMd5) {
|
||||
if ($isMd5) {
|
||||
return $dbForPlatform->getDocument('rules', md5($domain));
|
||||
}
|
||||
|
||||
return $dbForPlatform->findOne('rules', [
|
||||
Query::equal('domain', [$domain]),
|
||||
]) ?? new Document();
|
||||
});
|
||||
|
||||
$permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence();
|
||||
|
||||
if (!$permitsCurrentProject && !$rule->isEmpty() && !empty($rule->getAttribute('projectId', ''))) {
|
||||
$trustedProjects = [];
|
||||
foreach (\explode(',', System::getEnv('_APP_CONSOLE_TRUSTED_PROJECTS', '')) as $trustedProject) {
|
||||
if (empty($trustedProject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$trustedProjects[] = $trustedProject;
|
||||
}
|
||||
|
||||
if (\in_array($rule->getAttribute('projectId', ''), $trustedProjects, true)) {
|
||||
$permitsCurrentProject = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$permitsCurrentProject) {
|
||||
return new Document();
|
||||
}
|
||||
|
||||
return $rule;
|
||||
};
|
||||
|
||||
$findDevKey = static function (Request $request, Document $project, array $servers, Authorization $authorization) use ($getDbForPlatform): Document {
|
||||
$devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', ''));
|
||||
$key = $project->find('secret', $devKey, 'devKeys');
|
||||
|
||||
if (!$key) {
|
||||
return new Document([]);
|
||||
}
|
||||
|
||||
$expire = $key->getAttribute('expire');
|
||||
if (!empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
|
||||
return new Document([]);
|
||||
}
|
||||
|
||||
$dbForPlatform = $getDbForPlatform($authorization);
|
||||
$accessedAt = $key->getAttribute('accessedAt', 0);
|
||||
|
||||
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
|
||||
$key->setAttribute('accessedAt', DatabaseDateTime::now());
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([
|
||||
'accessedAt' => $key->getAttribute('accessedAt'),
|
||||
])));
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
|
||||
$sdkValidator = new WhiteList($servers, true);
|
||||
$sdk = \strtolower($request->getHeader('x-sdk-name', 'UNKNOWN'));
|
||||
|
||||
if ($sdk !== 'UNKNOWN' && $sdkValidator->isValid($sdk)) {
|
||||
$sdks = $key->getAttribute('sdks', []);
|
||||
|
||||
if (!\in_array($sdk, $sdks, true)) {
|
||||
$sdks[] = $sdk;
|
||||
$key->setAttribute('sdks', $sdks);
|
||||
$key->setAttribute('accessedAt', DatabaseDateTime::now());
|
||||
|
||||
$key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([
|
||||
'sdks' => $key->getAttribute('sdks'),
|
||||
'accessedAt' => $key->getAttribute('accessedAt'),
|
||||
])));
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
}
|
||||
|
||||
return $key;
|
||||
};
|
||||
|
||||
$container->set('authorization', function () {
|
||||
return new Authorization();
|
||||
}, []);
|
||||
|
||||
$container->set('project', function (Request $request, Document $console, Authorization $authorization) use ($getProjectId, $getDbForPlatform) {
|
||||
$projectId = $getProjectId($request);
|
||||
|
||||
if (empty($projectId) || $projectId === 'console') {
|
||||
return $console;
|
||||
}
|
||||
|
||||
$dbForPlatform = $getDbForPlatform($authorization);
|
||||
|
||||
return $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
|
||||
}, ['request', 'console', 'authorization']);
|
||||
|
||||
$container->set('originValidator', function (array $platform, Request $request, Document $project, array $servers, Authorization $authorization) use ($findDevKey, $findRule) {
|
||||
$devKey = $findDevKey($request, $project, $servers, $authorization);
|
||||
|
||||
if (!$devKey->isEmpty()) {
|
||||
return new URL();
|
||||
}
|
||||
|
||||
$allowedHostnames = [...($platform['hostnames'] ?? [])];
|
||||
if (!$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$allowedHostnames = [...$allowedHostnames, ...Platform::getHostnames($project->getAttribute('platforms', []))];
|
||||
}
|
||||
|
||||
$rule = $findRule($request, $project, $authorization);
|
||||
if (!$rule->isEmpty() && !empty($rule->getAttribute('domain', ''))) {
|
||||
$allowedHostnames[] = $rule->getAttribute('domain', '');
|
||||
}
|
||||
|
||||
$originHostname = \parse_url($request->getOrigin(), PHP_URL_HOST);
|
||||
$refererHostname = \parse_url($request->getReferer(), PHP_URL_HOST);
|
||||
$hostname = $originHostname ?: $refererHostname;
|
||||
|
||||
if ($request->getMethod() === 'OPTIONS' && !empty($hostname)) {
|
||||
$allowedHostnames[] = $hostname;
|
||||
}
|
||||
|
||||
$allowedSchemes = [...($platform['schemas'] ?? [])];
|
||||
if (!$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$allowedSchemes[] = 'exp';
|
||||
$allowedSchemes[] = 'appwrite-callback-' . $project->getId();
|
||||
$allowedSchemes = [...$allowedSchemes, ...Platform::getSchemes($project->getAttribute('platforms', []))];
|
||||
}
|
||||
|
||||
return new Origin(\array_unique($allowedHostnames), \array_unique($allowedSchemes));
|
||||
}, ['platform', 'request', 'project', 'servers', 'authorization']);
|
||||
|
||||
$container->set('user', function (Request $request, Document $project, Document $console, Authorization $authorization) use ($getMode, $getDbForPlatform, $getDbForProject) {
|
||||
$mode = $getMode($request, $project);
|
||||
$store = new Store();
|
||||
$proofForToken = new Token();
|
||||
$proofForToken->setHash(new Sha());
|
||||
|
||||
$authorization->setDefaultStatus(true);
|
||||
|
||||
$dbForPlatform = $getDbForPlatform($authorization);
|
||||
$dbForProject = $getDbForProject($project, $authorization);
|
||||
|
||||
$store->setKey('a_session_' . $project->getId());
|
||||
if ($mode === APP_MODE_ADMIN) {
|
||||
$store->setKey('a_session_' . $console->getId());
|
||||
}
|
||||
|
||||
$store->decode(
|
||||
$request->getCookie(
|
||||
$store->getKey(),
|
||||
$request->getCookie($store->getKey() . '_legacy', '')
|
||||
)
|
||||
);
|
||||
|
||||
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
|
||||
$sessionHeader = $request->getHeader('x-appwrite-session', '');
|
||||
|
||||
if (!empty($sessionHeader)) {
|
||||
$store->decode($sessionHeader);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
|
||||
$fallback = \json_decode($request->getHeader('x-fallback-cookies', ''), true);
|
||||
$store->decode((\is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : '');
|
||||
}
|
||||
|
||||
$user = null;
|
||||
if ($mode === APP_MODE_ADMIN) {
|
||||
/** @var User $user */
|
||||
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
|
||||
} else {
|
||||
if ($project->isEmpty()) {
|
||||
$user = new User([]);
|
||||
} elseif (!empty($store->getProperty('id', ''))) {
|
||||
if ($project->getId() === 'console') {
|
||||
/** @var User $user */
|
||||
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
|
||||
} else {
|
||||
/** @var User $user */
|
||||
$user = $dbForProject->getDocument('users', $store->getProperty('id', ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!$user
|
||||
|| $user->isEmpty()
|
||||
|| !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken)
|
||||
) {
|
||||
$user = new User([]);
|
||||
}
|
||||
|
||||
$authJWT = $request->getHeader('x-appwrite-jwt', '');
|
||||
if (!empty($authJWT) && !$project->isEmpty()) {
|
||||
if (!$user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_JWT_AND_COOKIE_SET);
|
||||
}
|
||||
|
||||
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
|
||||
|
||||
try {
|
||||
$payload = $jwt->decode($authJWT);
|
||||
} catch (JWTException $error) {
|
||||
throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage());
|
||||
}
|
||||
|
||||
$jwtUserId = $payload['userId'] ?? '';
|
||||
if (!empty($jwtUserId)) {
|
||||
if ($mode === APP_MODE_ADMIN) {
|
||||
$user = $dbForPlatform->getDocument('users', $jwtUserId);
|
||||
} else {
|
||||
$user = $dbForProject->getDocument('users', $jwtUserId);
|
||||
}
|
||||
}
|
||||
|
||||
$jwtSessionId = $payload['sessionId'] ?? '';
|
||||
if (!empty($jwtSessionId) && empty($user->find('$id', $jwtSessionId, 'sessions'))) {
|
||||
$user = new User([]);
|
||||
}
|
||||
}
|
||||
|
||||
$accountKey = $request->getHeader('x-appwrite-key', '');
|
||||
$accountKeyUserId = $request->getHeader('x-appwrite-user', '');
|
||||
|
||||
if (!empty($accountKeyUserId) && !empty($accountKey)) {
|
||||
if (!$user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
|
||||
}
|
||||
|
||||
$accountKeyUser = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId));
|
||||
if (!$accountKeyUser->isEmpty()) {
|
||||
$key = $accountKeyUser->find(
|
||||
key: 'secret',
|
||||
find: $accountKey,
|
||||
subject: 'keys'
|
||||
);
|
||||
|
||||
if (!empty($key)) {
|
||||
$expire = $key->getAttribute('expire');
|
||||
if (!empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
|
||||
throw new Exception(Exception::ACCOUNT_KEY_EXPIRED);
|
||||
}
|
||||
|
||||
$user = $accountKeyUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$impersonateUserId = $request->getHeader('x-appwrite-impersonate-user-id', '');
|
||||
$impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', '');
|
||||
$impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', '');
|
||||
|
||||
if (!$user->isEmpty() && $user->getAttribute('impersonator', false)) {
|
||||
$userDb = ($mode === APP_MODE_ADMIN || $project->getId() === 'console') ? $dbForPlatform : $dbForProject;
|
||||
$targetUser = null;
|
||||
|
||||
if (!empty($impersonateUserId)) {
|
||||
$targetUser = $authorization->skip(fn () => $userDb->getDocument('users', $impersonateUserId));
|
||||
} elseif (!empty($impersonateEmail)) {
|
||||
$targetUser = $authorization->skip(fn () => $userDb->findOne('users', [
|
||||
Query::equal('email', [\strtolower($impersonateEmail)]),
|
||||
]));
|
||||
} elseif (!empty($impersonatePhone)) {
|
||||
$targetUser = $authorization->skip(fn () => $userDb->findOne('users', [
|
||||
Query::equal('phone', [$impersonatePhone]),
|
||||
]));
|
||||
}
|
||||
|
||||
if ($targetUser !== null && !$targetUser->isEmpty()) {
|
||||
$impersonator = clone $user;
|
||||
$user = clone $targetUser;
|
||||
$user->setAttribute('impersonatorUserId', $impersonator->getId());
|
||||
$user->setAttribute('impersonatorUserInternalId', $impersonator->getSequence());
|
||||
$user->setAttribute('impersonatorUserName', $impersonator->getAttribute('name', ''));
|
||||
$user->setAttribute('impersonatorUserEmail', $impersonator->getAttribute('email', ''));
|
||||
$user->setAttribute('impersonatorAccessedAt', $impersonator->getAttribute('accessedAt', 0));
|
||||
}
|
||||
}
|
||||
|
||||
$dbForPlatform->setMetadata('user', $user->getId());
|
||||
$dbForProject->setMetadata('user', $user->getId());
|
||||
|
||||
return $user;
|
||||
}, ['request', 'project', 'console', 'authorization']);
|
||||
};
|
||||
+23
-49
@@ -6,7 +6,6 @@ use Appwrite\Hooks\Hooks;
|
||||
use Appwrite\PubSub\Adapter\Redis as PubSub;
|
||||
use Appwrite\URL\URL as AppwriteURL;
|
||||
use MaxMind\Db\Reader;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Swoole\Database\PDOProxy;
|
||||
use Utopia\Cache\Adapter\Redis as RedisCache;
|
||||
use Utopia\Config\Config;
|
||||
@@ -25,6 +24,7 @@ use Utopia\Logger\Adapter\LogOwl;
|
||||
use Utopia\Logger\Adapter\Raygun;
|
||||
use Utopia\Logger\Adapter\Sentry;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Messaging\Adapter\Email\SMTP;
|
||||
use Utopia\Mongo\Client as MongoClient;
|
||||
use Utopia\Pools\Adapter\Stack as StackPool;
|
||||
use Utopia\Pools\Adapter\Swoole as SwoolePool;
|
||||
@@ -56,7 +56,7 @@ $register->set('logger', function () {
|
||||
}
|
||||
|
||||
try {
|
||||
$loggingProvider = new DSN($providerConfig ?? '');
|
||||
$loggingProvider = new DSN($providerConfig);
|
||||
|
||||
$providerName = $loggingProvider->getScheme();
|
||||
$providerConfig = match ($providerName) {
|
||||
@@ -76,7 +76,7 @@ $register->set('logger', function () {
|
||||
};
|
||||
}
|
||||
|
||||
if (empty($providerName) || empty($providerConfig)) {
|
||||
if (empty($providerName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ $register->set('realtimeLogger', function () {
|
||||
default => ['key' => $loggingProvider->getHost()],
|
||||
};
|
||||
|
||||
if (empty($providerName) || empty($providerConfig)) {
|
||||
if (empty($providerName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -242,22 +242,11 @@ $register->set('pools', function () {
|
||||
],
|
||||
];
|
||||
|
||||
$maxConnections = System::getEnv('_APP_CONNECTIONS_MAX', 151);
|
||||
$instanceConnections = $maxConnections / System::getEnv('_APP_POOL_CLIENTS', 14);
|
||||
$maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151);
|
||||
$instanceConnections = $maxConnections / (int) System::getEnv('_APP_POOL_CLIENTS', 14);
|
||||
|
||||
$multiprocessing = System::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled';
|
||||
|
||||
if ($multiprocessing) {
|
||||
$workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
|
||||
} else {
|
||||
$workerCount = 1;
|
||||
}
|
||||
|
||||
if ($workerCount > $instanceConnections) {
|
||||
throw new \Exception('Pool size is too small. Increase the number of allowed database connections or decrease the number of workers.', 500);
|
||||
}
|
||||
|
||||
$poolSize = (int)($instanceConnections / $workerCount);
|
||||
$workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
|
||||
$poolSize = max(1, (int)($instanceConnections / $workerCount));
|
||||
|
||||
foreach ($connections as $key => $connection) {
|
||||
$type = $connection['type'] ?? '';
|
||||
@@ -308,7 +297,7 @@ $register->set('pools', function () {
|
||||
]);
|
||||
});
|
||||
},
|
||||
'mongodb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase, $dsn) {
|
||||
'mongodb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) {
|
||||
try {
|
||||
$mongo = new MongoClient($dsnDatabase, $dsnHost, (int)$dsnPort, $dsnUser, $dsnPass, false);
|
||||
@$mongo->connect();
|
||||
@@ -433,35 +422,20 @@ $register->set('db', function () {
|
||||
});
|
||||
|
||||
$register->set('smtp', function () {
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
$mail->isSMTP();
|
||||
|
||||
$username = System::getEnv('_APP_SMTP_USERNAME');
|
||||
$password = System::getEnv('_APP_SMTP_PASSWORD');
|
||||
|
||||
$mail->XMailer = 'Appwrite Mailer';
|
||||
$mail->Host = System::getEnv('_APP_SMTP_HOST', 'smtp');
|
||||
$mail->Port = System::getEnv('_APP_SMTP_PORT', 25);
|
||||
$mail->SMTPAuth = !empty($username) && !empty($password);
|
||||
$mail->Username = $username;
|
||||
$mail->Password = $password;
|
||||
$mail->SMTPSecure = System::getEnv('_APP_SMTP_SECURE', '');
|
||||
$mail->SMTPAutoTLS = false;
|
||||
$mail->SMTPKeepAlive = true;
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->Timeout = 10; /* Connection timeout */
|
||||
$mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */
|
||||
|
||||
$from = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
|
||||
$email = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
|
||||
|
||||
$mail->setFrom($email, $from);
|
||||
$mail->addReplyTo($email, $from);
|
||||
|
||||
$mail->isHTML(true);
|
||||
|
||||
return $mail;
|
||||
$username = System::getEnv('_APP_SMTP_USERNAME', '');
|
||||
$password = System::getEnv('_APP_SMTP_PASSWORD', '');
|
||||
return new SMTP(
|
||||
host: System::getEnv('_APP_SMTP_HOST', 'smtp'),
|
||||
port: (int) System::getEnv('_APP_SMTP_PORT', 25),
|
||||
username: $username,
|
||||
password: $password,
|
||||
smtpSecure: System::getEnv('_APP_SMTP_SECURE', ''),
|
||||
smtpAutoTLS: false,
|
||||
xMailer: 'Appwrite Mailer',
|
||||
timeout: 10,
|
||||
keepAlive: true,
|
||||
timelimit: 30,
|
||||
);
|
||||
});
|
||||
$register->set('geodb', function () {
|
||||
return new Reader(__DIR__ . '/../assets/dbip/dbip-country-lite-2025-12.mmdb');
|
||||
|
||||
+53
-1372
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,451 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Screenshot;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Utopia\Audit\Adapter\Database as AdapterDatabase;
|
||||
use Utopia\Audit\Audit as UtopiaAudit;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Adapter\Pool as DatabasePool;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DI\Container;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Registry\Registry;
|
||||
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter as Telemetry;
|
||||
|
||||
/**
|
||||
* Register per-job resources on the given container.
|
||||
* These resources depend on the queue message or keep mutable state and
|
||||
* must be fresh for each worker job.
|
||||
*/
|
||||
return function (Container $container): void {
|
||||
$container->set('log', fn () => new Log(), []);
|
||||
|
||||
$container->set('usage', fn () => new Context(), []);
|
||||
|
||||
$container->set('authorization', function () {
|
||||
$authorization = new Authorization();
|
||||
$authorization->disable();
|
||||
|
||||
return $authorization;
|
||||
}, []);
|
||||
|
||||
$container->set('dbForPlatform', function (Cache $cache, Group $pools, Authorization $authorization) {
|
||||
$adapter = new DatabasePool($pools->get('console'));
|
||||
$dbForPlatform = new Database($adapter, $cache);
|
||||
|
||||
$dbForPlatform
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setNamespace('_console')
|
||||
->setDocumentType('users', User::class);
|
||||
|
||||
return $dbForPlatform;
|
||||
}, ['cache', 'pools', 'authorization']);
|
||||
|
||||
$container->set('project', function ($message, Database $dbForPlatform) {
|
||||
$payload = $message->getPayload() ?? [];
|
||||
$project = new Document($payload['project'] ?? []);
|
||||
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $project;
|
||||
}
|
||||
|
||||
return $dbForPlatform->getDocument('projects', $project->getId());
|
||||
}, ['message', 'dbForPlatform']);
|
||||
|
||||
$container->set('dbForProject', function (Cache $cache, Group $pools, Document $project, Database $dbForPlatform, Authorization $authorization) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
}
|
||||
|
||||
try {
|
||||
$dsn = new DSN($project->getAttribute('database'));
|
||||
} catch (\InvalidArgumentException) {
|
||||
// TODO: Temporary until all projects are using shared tables
|
||||
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
|
||||
}
|
||||
|
||||
$adapter = new DatabasePool($pools->get($dsn->getHost()));
|
||||
$database = new Database($adapter, $cache);
|
||||
$database->setDocumentType('users', User::class);
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
|
||||
return $database;
|
||||
}, ['cache', 'pools', 'project', 'dbForPlatform', 'authorization']);
|
||||
|
||||
$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) {
|
||||
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
|
||||
|
||||
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
}
|
||||
|
||||
try {
|
||||
$dsn = new DSN($project->getAttribute('database'));
|
||||
} catch (\InvalidArgumentException) {
|
||||
// TODO: Temporary until all projects are using shared tables
|
||||
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
|
||||
}
|
||||
|
||||
if (isset($databases[$dsn->getHost()])) {
|
||||
$database = $databases[$dsn->getHost()];
|
||||
$database->setAuthorization($authorization);
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
return $database;
|
||||
}
|
||||
|
||||
$adapter = new DatabasePool($pools->get($dsn->getHost()));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
$databases[$dsn->getHost()] = $database;
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
|
||||
|
||||
$container->set('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) {
|
||||
return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database {
|
||||
$projectDocument ??= $project;
|
||||
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
|
||||
$databaseType = $database->getAttribute('type', '');
|
||||
|
||||
// Backwards-compatibility: older or seeded legacy databases may not have a DSN stored
|
||||
// in the "database" attribute. In that case, fall back to the project's database DSN.
|
||||
if ($databaseDSN === '') {
|
||||
$databaseDSN = $projectDocument->getAttribute('database', '');
|
||||
}
|
||||
|
||||
try {
|
||||
$databaseDSN = new DSN($databaseDSN);
|
||||
} catch (\InvalidArgumentException) {
|
||||
$databaseDSN = new DSN('mysql://' . $databaseDSN);
|
||||
}
|
||||
|
||||
try {
|
||||
$dsn = new DSN($projectDocument->getAttribute('database'));
|
||||
} catch (\InvalidArgumentException) {
|
||||
// Temporary fallback until all projects use shared tables
|
||||
$dsn = new DSN('mysql://' . $projectDocument->getAttribute('database'));
|
||||
}
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$databaseHost = $databaseDSN->getHost();
|
||||
$pool = $pools->get($databaseHost);
|
||||
|
||||
$adapter = new DatabasePool($pool);
|
||||
$database = new Database($adapter, $cache);
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization);
|
||||
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
|
||||
|
||||
$sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')));
|
||||
|
||||
// For separate pools (documentsdb/vectorsdb), check their own shared tables config.
|
||||
// If not configured, use dedicated mode to avoid cross-engine tenant type mismatches.
|
||||
if ($databaseHost !== $dsn->getHost()) {
|
||||
$dbTypeSharedTables = match ($databaseType) {
|
||||
DOCUMENTSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''))),
|
||||
VECTORSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''))),
|
||||
default => [],
|
||||
};
|
||||
|
||||
if (\in_array($databaseHost, $dbTypeSharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($projectDocument->getSequence())
|
||||
->setNamespace($databaseDSN->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $projectDocument->getSequence());
|
||||
}
|
||||
} elseif (\in_array($dsn->getHost(), $sharedTables, true)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($projectDocument->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $projectDocument->getSequence());
|
||||
}
|
||||
|
||||
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
return $database;
|
||||
};
|
||||
}, ['cache', 'register', 'project', 'authorization']);
|
||||
|
||||
$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
$database = null;
|
||||
|
||||
return function (?Document $project = null) use ($pools, $cache, $authorization, &$database) {
|
||||
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant($project->getSequence());
|
||||
|
||||
return $database;
|
||||
}
|
||||
|
||||
$adapter = new DatabasePool($pools->get('logs'));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setSharedTables(true)
|
||||
->setNamespace('logsV1')
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
|
||||
|
||||
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant($project->getSequence());
|
||||
}
|
||||
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
$container->set('abuseRetention', function () {
|
||||
return \time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day
|
||||
}, []);
|
||||
|
||||
$container->set('auditRetention', function (Document $project) {
|
||||
if ($project->getId() === 'console') {
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
|
||||
}
|
||||
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
|
||||
}, ['project']);
|
||||
|
||||
$container->set('executionRetention', function () {
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
|
||||
}, []);
|
||||
|
||||
$container->set('queueForDatabase', function (Publisher $publisher) {
|
||||
return new EventDatabase($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForMessaging', function (Publisher $publisher) {
|
||||
return new Messaging($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForMails', function (Publisher $publisher) {
|
||||
return new Mail($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForBuilds', function (Publisher $publisher) {
|
||||
return new Build($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForScreenshots', function (Publisher $publisher) {
|
||||
return new Screenshot($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForDeletes', function (Publisher $publisher) {
|
||||
return new Delete($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForEvents', function (Publisher $publisher) {
|
||||
return new Event($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForAudits', function (Publisher $publisher) {
|
||||
return new Audit($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForWebhooks', function (Publisher $publisher) {
|
||||
return new Webhook($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForFunctions', function (Publisher $publisher) {
|
||||
return new Func($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForRealtime', function () {
|
||||
return new Realtime();
|
||||
}, []);
|
||||
|
||||
$container->set('queueForCertificates', function (Publisher $publisher) {
|
||||
return new Certificate($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('deviceForSites', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
$container->set('deviceForMigrations', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
$container->set('deviceForFunctions', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
$container->set('deviceForFiles', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
$container->set('deviceForBuilds', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
$container->set('deviceForCache', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
$container->set('logError', function (Registry $register, Document $project) {
|
||||
return function (Throwable $error, string $namespace, string $action, ?array $extras = null) use ($register, $project) {
|
||||
$logger = $register->get('logger');
|
||||
|
||||
if ($logger) {
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
$log = new Log();
|
||||
$log->setNamespace($namespace);
|
||||
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('verboseType', \get_class($error));
|
||||
$log->addTag('projectId', $project->getId() ?? '');
|
||||
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
|
||||
if ($error->getPrevious() !== null) {
|
||||
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
|
||||
$log->addExtra('previousMessage', $error->getPrevious()->getMessage());
|
||||
}
|
||||
$log->addExtra('previousFile', $error->getPrevious()->getFile());
|
||||
$log->addExtra('previousLine', $error->getPrevious()->getLine());
|
||||
}
|
||||
|
||||
foreach (($extras ?? []) as $key => $value) {
|
||||
$log->addExtra($key, $value);
|
||||
}
|
||||
|
||||
$log->setAction($action);
|
||||
|
||||
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
try {
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Error log pushed with status code: ' . $responseCode);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Error pushing log: ' . $th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
Console::warning("Failed: {$error->getMessage()}");
|
||||
Console::warning($error->getTraceAsString());
|
||||
|
||||
if ($error->getPrevious() !== null) {
|
||||
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
|
||||
Console::warning("Previous Failed: {$error->getPrevious()->getMessage()}");
|
||||
}
|
||||
Console::warning("Previous File: {$error->getPrevious()->getFile()} Line: {$error->getPrevious()->getLine()}");
|
||||
}
|
||||
};
|
||||
}, ['register', 'project']);
|
||||
|
||||
$container->set('getAudit', function (Database $dbForPlatform, callable $getProjectDB) {
|
||||
return function (Document $project) use ($dbForPlatform, $getProjectDB) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
$adapter = new AdapterDatabase($dbForPlatform);
|
||||
|
||||
return new UtopiaAudit($adapter);
|
||||
}
|
||||
|
||||
$dbForProject = $getProjectDB($project);
|
||||
$adapter = new AdapterDatabase($dbForProject);
|
||||
|
||||
return new UtopiaAudit($adapter);
|
||||
};
|
||||
}, ['dbForPlatform', 'getProjectDB']);
|
||||
|
||||
$container->set('executionsRetentionCount', function (Document $project, array $plan) {
|
||||
if ($project->getId() === 'console' || empty($plan)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) ($plan['executionsRetentionCount'] ?? 100);
|
||||
}, ['project', 'plan']);
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Bus\Listeners\Log;
|
||||
use Appwrite\Bus\Listeners\Mails;
|
||||
use Appwrite\Bus\Listeners\Usage;
|
||||
|
||||
return [
|
||||
new Log(),
|
||||
new Mails(),
|
||||
new Usage(),
|
||||
];
|
||||
|
||||
+168
-46
@@ -33,8 +33,8 @@ use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DI\Container;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Registry\Registry;
|
||||
@@ -43,11 +43,13 @@ use Utopia\Telemetry\Adapter\None as NoTelemetry;
|
||||
use Utopia\WebSocket\Adapter;
|
||||
use Utopia\WebSocket\Server;
|
||||
|
||||
/**
|
||||
* @var Registry $register
|
||||
*/
|
||||
require_once __DIR__ . '/init.php';
|
||||
|
||||
/** @var Registry $register */
|
||||
$register = $GLOBALS['register'] ?? throw new \RuntimeException('Registry not initialized');
|
||||
|
||||
$registerConnectionResources ??= require __DIR__ . '/init/realtime/connection.php';
|
||||
|
||||
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
|
||||
|
||||
// Log uncaught exceptions in one line instead of relying on Swoole's full backtrace dump
|
||||
@@ -237,10 +239,14 @@ if (!function_exists('getTelemetry')) {
|
||||
if (!function_exists('triggerStats')) {
|
||||
function triggerStats(array $event, string $projectId): void
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
global $container;
|
||||
$container->set('pools', function ($register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
|
||||
$realtime = getRealtime();
|
||||
|
||||
/**
|
||||
@@ -320,14 +326,14 @@ if (!function_exists('logError')) {
|
||||
|
||||
$server->error(logError(...));
|
||||
|
||||
$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument) {
|
||||
$server->onStart(function () use ($stats, $containerId, &$statsDocument) {
|
||||
sleep(5); // wait for the initial database schema to be ready
|
||||
Console::success('Server started successfully');
|
||||
|
||||
/**
|
||||
* Create document for this worker to share stats across Containers.
|
||||
*/
|
||||
go(function () use ($register, $containerId, &$statsDocument) {
|
||||
go(function () use ($containerId, &$statsDocument) {
|
||||
$attempts = 0;
|
||||
$database = getConsoleDB();
|
||||
|
||||
@@ -357,7 +363,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
|
||||
*/
|
||||
// TODO: Remove this if check once it doesn't cause issues for cloud
|
||||
if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') {
|
||||
Timer::tick(5000, function () use ($register, $stats, &$statsDocument) {
|
||||
Timer::tick(5000, function () use ($stats, &$statsDocument) {
|
||||
$payload = [];
|
||||
foreach ($stats as $projectId => $value) {
|
||||
$payload[$projectId] = $stats->get($projectId, 'connectionsTotal');
|
||||
@@ -396,7 +402,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
$attempts = 0;
|
||||
$start = time();
|
||||
|
||||
Timer::tick(5000, function () use ($server, $register, $realtime, $stats) {
|
||||
Timer::tick(5000, function () use ($server, $realtime, $stats) {
|
||||
/**
|
||||
* Sending current connections to project channels on the console project every 5 seconds.
|
||||
*/
|
||||
@@ -442,7 +448,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
]
|
||||
];
|
||||
|
||||
$server->send($realtime->getSubscribers($event), json_encode([
|
||||
$server->send(array_keys($realtime->getSubscribers($event)), json_encode([
|
||||
'type' => 'event',
|
||||
'data' => $event['data']
|
||||
]));
|
||||
@@ -549,7 +555,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
if (Http::isDevelopment() && !empty($receivers)) {
|
||||
if (System::getEnv('_APP_ENV', 'production') === 'development' && !empty($receivers)) {
|
||||
Console::log("[Debug][Worker {$workerId}] Receivers: " . count($receivers));
|
||||
Console::log("[Debug][Worker {$workerId}] Connection IDs: " . json_encode(array_keys($receivers)));
|
||||
Console::log("[Debug][Worker {$workerId}] Matched: " . json_encode(array_values($receivers)));
|
||||
@@ -615,16 +621,17 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
Console::error('Failed to restart pub/sub...');
|
||||
});
|
||||
|
||||
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime) {
|
||||
$app = new Http('UTC');
|
||||
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $registerConnectionResources) {
|
||||
global $container;
|
||||
$request = new Request($request);
|
||||
$response = new Response(new SwooleResponse());
|
||||
|
||||
Console::info("Connection open (user: {$connection})");
|
||||
|
||||
Http::setResource('pools', fn () => $register->get('pools'));
|
||||
Http::setResource('request', fn () => $request);
|
||||
Http::setResource('response', fn () => $response);
|
||||
$connectionContainer = new Container($container);
|
||||
$connectionContainer->set('request', fn () => $request);
|
||||
|
||||
$registerConnectionResources($connectionContainer);
|
||||
|
||||
$project = null;
|
||||
$logUser = null;
|
||||
@@ -632,8 +639,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
|
||||
try {
|
||||
/** @var Document $project */
|
||||
$project = $app->getResource('project');
|
||||
$authorization = $app->getResource('authorization');
|
||||
$project = $connectionContainer->get('project');
|
||||
$authorization = $connectionContainer->get('authorization');
|
||||
|
||||
/*
|
||||
* Project Check
|
||||
@@ -642,13 +649,15 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing or unknown project ID');
|
||||
}
|
||||
|
||||
$timelimit = $app->getResource('timelimit');
|
||||
$user = $app->getResource('user'); /** @var User $user */
|
||||
$timelimit = $connectionContainer->get('timelimit');
|
||||
$user = $connectionContainer->get('user'); /** @var User $user */
|
||||
$logUser = $user;
|
||||
|
||||
$apis = $project->getAttribute('apis', []);
|
||||
// Websocket is what to check, but realtime is checked too for backwards compatibility
|
||||
$websocketEnabled = $apis['websocket'] ?? $apis['realtime'] ?? true;
|
||||
if (
|
||||
array_key_exists('realtime', $project->getAttribute('apis', []))
|
||||
&& !$project->getAttribute('apis', [])['realtime']
|
||||
!$websocketEnabled
|
||||
&& !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
@@ -688,7 +697,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
* Skip this check for non-web platforms which are not required to send an origin header.
|
||||
*/
|
||||
$origin = $request->getOrigin();
|
||||
$originValidator = $app->getResource('originValidator');
|
||||
$originValidator = $connectionContainer->get('originValidator');
|
||||
|
||||
if (!empty($origin) && !$originValidator->isValid($origin) && $project->getId() !== 'console') {
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
|
||||
@@ -698,11 +707,43 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
|
||||
$channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId());
|
||||
|
||||
$updateStats = static function (string $projectId, ?string $teamId, string $payloadJson) use ($register, $stats): void {
|
||||
$register->get('telemetry.connectionCounter')->add(1);
|
||||
$register->get('telemetry.connectionCreatedCounter')->add(1);
|
||||
|
||||
$stats->set($projectId, [
|
||||
'projectId' => $projectId,
|
||||
'teamId' => $teamId
|
||||
]);
|
||||
$stats->incr($projectId, 'connections');
|
||||
$stats->incr($projectId, 'connectionsTotal');
|
||||
|
||||
triggerStats([
|
||||
METRIC_REALTIME_CONNECTIONS => 1,
|
||||
METRIC_REALTIME_OUTBOUND => \strlen($payloadJson),
|
||||
], $projectId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Channels Check
|
||||
*/
|
||||
if (empty($channels)) {
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing channels');
|
||||
// in case of message based 'subscribe' channels will be empty at first and only projectId and roles will be available
|
||||
$sanitizedUser = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT);
|
||||
$connectedPayloadJson = json_encode([
|
||||
'type' => 'connected',
|
||||
'data' => [
|
||||
'channels' => [],
|
||||
'subscriptions' => [],
|
||||
'user' => $sanitizedUser
|
||||
]
|
||||
]);
|
||||
|
||||
$realtime->subscribe($project->getId(), $connection, '', $roles, [], [], $user->getId());
|
||||
$realtime->connections[$connection]['authorization'] = $authorization;
|
||||
$server->send([$connection], $connectedPayloadJson);
|
||||
$updateStats($project->getId(), $project->getAttribute('teamId'), $connectedPayloadJson);
|
||||
return;
|
||||
}
|
||||
|
||||
$names = array_keys($channels);
|
||||
@@ -726,7 +767,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
$subscriptionId,
|
||||
$roles,
|
||||
$subscription['channels'],
|
||||
$subscription['queries']
|
||||
$subscription['queries'],
|
||||
$user->getId()
|
||||
);
|
||||
|
||||
$mapping[$index] = $subscriptionId;
|
||||
@@ -746,20 +788,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
]);
|
||||
|
||||
$server->send([$connection], $connectedPayloadJson);
|
||||
|
||||
$register->get('telemetry.connectionCounter')->add(1);
|
||||
$register->get('telemetry.connectionCreatedCounter')->add(1);
|
||||
|
||||
$stats->set($project->getId(), [
|
||||
'projectId' => $project->getId(),
|
||||
'teamId' => $project->getAttribute('teamId')
|
||||
]);
|
||||
$stats->incr($project->getId(), 'connections');
|
||||
$stats->incr($project->getId(), 'connectionsTotal');
|
||||
|
||||
$connectedOutboundBytes = \strlen($connectedPayloadJson);
|
||||
|
||||
triggerStats([METRIC_REALTIME_CONNECTIONS => 1, METRIC_REALTIME_OUTBOUND => $connectedOutboundBytes], $project->getId());
|
||||
$updateStats($project->getId(), $project->getAttribute('teamId'), $connectedPayloadJson);
|
||||
|
||||
|
||||
} catch (Throwable $th) {
|
||||
@@ -775,7 +804,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
|
||||
// sanitize 0 && 5xx errors
|
||||
$realtimeViolation = $th instanceof AppwriteException && $th->getType() === AppwriteException::REALTIME_POLICY_VIOLATION;
|
||||
if (($code === 0 || $code >= 500) && !$realtimeViolation && !Http::isDevelopment()) {
|
||||
if (($code === 0 || $code >= 500) && !$realtimeViolation && System::getEnv('_APP_ENV', 'production') !== 'development') {
|
||||
$message = 'Error: Server Error';
|
||||
}
|
||||
|
||||
@@ -790,7 +819,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
$server->send([$connection], json_encode($response));
|
||||
$server->close($connection, $code);
|
||||
|
||||
if (Http::isDevelopment()) {
|
||||
if (System::getEnv('_APP_ENV', 'production') === 'development') {
|
||||
Console::error('[Error] Connection Error');
|
||||
Console::error('[Error] Code: ' . $response['data']['code']);
|
||||
Console::error('[Error] Message: ' . $response['data']['message']);
|
||||
@@ -798,10 +827,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
}
|
||||
});
|
||||
|
||||
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) {
|
||||
$server->onMessage(function (int $connection, string $message) use ($server, $realtime, $containerId) {
|
||||
$project = null;
|
||||
$authorization = null;
|
||||
|
||||
try {
|
||||
$rawSize = \strlen($message);
|
||||
$response = new Response(new SwooleResponse());
|
||||
@@ -810,7 +838,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
// Get authorization from connection (stored during onOpen)
|
||||
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
|
||||
if ($authorization === null) {
|
||||
$authorization = new Authorization('');
|
||||
$authorization = new Authorization();
|
||||
}
|
||||
|
||||
$database = getConsoleDB();
|
||||
@@ -927,7 +955,8 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
$subscriptionId,
|
||||
$roles,
|
||||
$subscription['channels'] ?? [],
|
||||
$queries
|
||||
$queries,
|
||||
$user->getId()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -961,6 +990,99 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
|
||||
break;
|
||||
|
||||
case 'subscribe':
|
||||
/**
|
||||
* Message based upsertion of a subscription
|
||||
* If subscriptionId is given then it will match subId of the connection and update the subscription with channels and queries
|
||||
* If non-existing subid is given or not given a new subid will be generated
|
||||
* Similar to what we have now -> two subscribe() block with same channels and queries still two different subscriptions
|
||||
*
|
||||
* structure of the payload -> array of maps
|
||||
* 'data' : [subscriptionId:"" , channels:[] , queries:[]]
|
||||
*/
|
||||
if (!is_array($message['data']) || !array_is_list($message['data'])) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
|
||||
}
|
||||
|
||||
$roles = $realtime->connections[$connection]['roles'] ?? [Role::guests()->toString()];
|
||||
$userId = $realtime->connections[$connection]['userId'] ?? '';
|
||||
|
||||
// bulk validation + parsing before subscribing
|
||||
$parsedPayloads = [];
|
||||
foreach ($message['data'] as $payload) {
|
||||
if (!\is_array($payload)) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Each subscribe payload must be an object.');
|
||||
}
|
||||
if (!array_key_exists('channels', $payload)) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'channels is not present in payload.');
|
||||
}
|
||||
if (!is_array($payload['channels']) || !array_is_list($payload['channels'])) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'channels is not a valid array.');
|
||||
}
|
||||
// registering the queries if not present and check in the same payload later on
|
||||
if (!array_key_exists('queries', $payload)) {
|
||||
$payload['queries'] = [];
|
||||
}
|
||||
if (!is_array($payload['queries']) || !array_is_list($payload['queries'])) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'queries is not a valid array.');
|
||||
}
|
||||
|
||||
$subscriptionId = \array_key_exists('subscriptionId', $payload)
|
||||
? $payload['subscriptionId']
|
||||
: ID::unique();
|
||||
|
||||
try {
|
||||
$convertedQueries = Realtime::convertQueries($payload['queries']);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Invalid query: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$parsedPayloads[] = [
|
||||
'subscriptionId' => $subscriptionId,
|
||||
'channels' => $payload['channels'],
|
||||
'queries' => $convertedQueries,
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($parsedPayloads as $parsedPayload) {
|
||||
$subscriptionId = $parsedPayload['subscriptionId'];
|
||||
$channels = \array_keys(Realtime::convertChannels($parsedPayload['channels'], $userId));
|
||||
$queries = $parsedPayload['queries'];
|
||||
$realtime->subscribe($projectId, $connection, $subscriptionId, $roles, $channels, $queries);
|
||||
}
|
||||
|
||||
// subscribe() overwrites the connection entry; restore auth so later onMessage uses the same context.
|
||||
$realtime->connections[$connection]['authorization'] = $authorization;
|
||||
|
||||
$responsePayload = json_encode([
|
||||
'type' => 'response',
|
||||
'data' => [
|
||||
'to' => 'subscribe',
|
||||
'success' => true,
|
||||
'subscriptions' => \array_map(function (array $parsedPayload) {
|
||||
return [
|
||||
'subscriptionId' => $parsedPayload['subscriptionId'],
|
||||
'channels' => $parsedPayload['channels'],
|
||||
'queries' => \array_map(fn ($q) => $q->toString(), $parsedPayload['queries']),
|
||||
];
|
||||
}, $parsedPayloads),
|
||||
]
|
||||
]);
|
||||
|
||||
$server->send([$connection], $responsePayload);
|
||||
|
||||
if ($project !== null && !$project->isEmpty()) {
|
||||
$subscribeOutboundBytes = \strlen($responsePayload);
|
||||
|
||||
if ($subscribeOutboundBytes > 0) {
|
||||
triggerStats([
|
||||
METRIC_REALTIME_OUTBOUND => $subscribeOutboundBytes,
|
||||
], $project->getId());
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message type is not valid.');
|
||||
}
|
||||
@@ -974,7 +1096,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
$message = $th->getMessage();
|
||||
|
||||
// sanitize 0 && 5xx errors
|
||||
if (($code === 0 || $code >= 500) && !Http::isDevelopment()) {
|
||||
if (($code === 0 || $code >= 500) && System::getEnv('_APP_ENV', 'production') !== 'development') {
|
||||
$message = 'Error: Server Error';
|
||||
}
|
||||
|
||||
|
||||
@@ -532,6 +532,35 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-executions:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
entrypoint: worker-executions
|
||||
<<: *x-logging
|
||||
container_name: appwrite-worker-executions
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
<?= $dbService ?>:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_ENV
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-functions:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
entrypoint: worker-functions
|
||||
@@ -964,7 +993,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
<<: *x-logging
|
||||
restart: unless-stopped
|
||||
stop_signal: SIGINT
|
||||
image: openruntimes/executor:0.7.22
|
||||
image: openruntimes/executor:0.11.4
|
||||
networks:
|
||||
- appwrite
|
||||
- runtimes
|
||||
|
||||
+41
-530
@@ -1,565 +1,69 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/init.php';
|
||||
$registerWorkerMessageResources = require __DIR__ . '/init/worker/message.php';
|
||||
|
||||
use Appwrite\Certificates\LetsEncrypt;
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Screenshot;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Executor\Executor;
|
||||
use Swoole\Runtime;
|
||||
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
|
||||
use Utopia\Audit\Adapter\Database as AdapterDatabase;
|
||||
use Utopia\Audit\Audit as UtopiaAudit;
|
||||
use Utopia\Cache\Adapter\Pool as CachePool;
|
||||
use Utopia\Cache\Adapter\Sharding;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Adapter\Pool as DatabasePool;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Platform\Service;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Queue\Adapter\Swoole;
|
||||
use Utopia\Queue\Broker\Pool as BrokerPool;
|
||||
use Utopia\Queue\Message;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Queue\Queue;
|
||||
use Utopia\Queue\Server;
|
||||
use Utopia\Registry\Registry;
|
||||
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter as Telemetry;
|
||||
use Utopia\Telemetry\Adapter\None as NoTelemetry;
|
||||
|
||||
Runtime::enableCoroutine();
|
||||
require_once __DIR__ . '/init/span.php';
|
||||
|
||||
global $register;
|
||||
Server::setResource('register', fn () => $register);
|
||||
global $container;
|
||||
$container->set('pools', function ($register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
|
||||
Server::setResource('authorization', function () {
|
||||
$container->set('authorization', function () {
|
||||
$authorization = new Authorization();
|
||||
$authorization->disable();
|
||||
|
||||
return $authorization;
|
||||
}, []);
|
||||
|
||||
Server::setResource('dbForPlatform', function (Cache $cache, Registry $register, Authorization $authorization) {
|
||||
$pools = $register->get('pools');
|
||||
$adapter = new DatabasePool($pools->get('console'));
|
||||
$dbForPlatform = new Database($adapter, $cache);
|
||||
$container->set('project', fn () => new Document([]), []);
|
||||
|
||||
$dbForPlatform
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setNamespace('_console')
|
||||
->setDocumentType('users', User::class);
|
||||
$container->set('log', fn () => new Log(), []);
|
||||
|
||||
return $dbForPlatform;
|
||||
}, ['cache', 'register', 'authorization']);
|
||||
|
||||
Server::setResource('project', function (Message $message, Database $dbForPlatform) {
|
||||
$payload = $message->getPayload() ?? [];
|
||||
$project = new Document($payload['project'] ?? []);
|
||||
|
||||
if ($project->getId() === 'console') {
|
||||
return $project;
|
||||
}
|
||||
|
||||
return $dbForPlatform->getDocument('projects', $project->getId());
|
||||
}, ['message', 'dbForPlatform']);
|
||||
|
||||
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForPlatform, Authorization $authorization) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
}
|
||||
|
||||
$pools = $register->get('pools');
|
||||
|
||||
try {
|
||||
$dsn = new DSN($project->getAttribute('database'));
|
||||
} catch (\InvalidArgumentException) {
|
||||
// TODO: Temporary until all projects are using shared tables
|
||||
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
|
||||
}
|
||||
|
||||
$adapter = new DatabasePool($pools->get($dsn->getHost()));
|
||||
$database = new Database($adapter, $cache);
|
||||
$database->setDocumentType('users', User::class);
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
|
||||
return $database;
|
||||
}, ['cache', 'register', 'message', 'project', 'dbForPlatform', 'authorization']);
|
||||
|
||||
Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) {
|
||||
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
|
||||
|
||||
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
}
|
||||
|
||||
try {
|
||||
$dsn = new DSN($project->getAttribute('database'));
|
||||
} catch (\InvalidArgumentException) {
|
||||
// TODO: Temporary until all projects are using shared tables
|
||||
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
|
||||
}
|
||||
|
||||
if (isset($databases[$dsn->getHost()])) {
|
||||
$database = $databases[$dsn->getHost()];
|
||||
$database->setAuthorization($authorization);
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
return $database;
|
||||
}
|
||||
|
||||
$adapter = new DatabasePool($pools->get($dsn->getHost()));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
$databases[$dsn->getHost()] = $database;
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
|
||||
|
||||
Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
$database = null;
|
||||
|
||||
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
|
||||
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant($project->getSequence());
|
||||
return $database;
|
||||
}
|
||||
|
||||
$adapter = new DatabasePool($pools->get('logs'));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setSharedTables(true)
|
||||
->setNamespace('logsV1')
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
|
||||
|
||||
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant($project->getSequence());
|
||||
}
|
||||
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) {
|
||||
return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database {
|
||||
$projectDocument ??= $project;
|
||||
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
|
||||
$databaseType = $database->getAttribute('type', '');
|
||||
|
||||
// Backwards‑compatibility: older or seeded legacy databases may not have a DSN stored
|
||||
// in the "database" attribute. In that case, fall back to the project's database DSN.
|
||||
if ($databaseDSN === '') {
|
||||
$databaseDSN = $projectDocument->getAttribute('database', '');
|
||||
}
|
||||
|
||||
try {
|
||||
$databaseDSN = new DSN($databaseDSN);
|
||||
} catch (\InvalidArgumentException) {
|
||||
$databaseDSN = new DSN('mysql://'.$databaseDSN);
|
||||
}
|
||||
|
||||
try {
|
||||
$dsn = new DSN($projectDocument->getAttribute('database'));
|
||||
} catch (\InvalidArgumentException) {
|
||||
// Temporary fallback until all projects use shared tables
|
||||
$dsn = new DSN('mysql://' . $projectDocument->getAttribute('database'));
|
||||
}
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$pool = $pools->get($databaseDSN->getHost());
|
||||
|
||||
$adapter = new DatabasePool($pool);
|
||||
$database = new Database($adapter, $cache);
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization);
|
||||
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables, true)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant((int) $projectDocument->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $projectDocument->getSequence());
|
||||
}
|
||||
|
||||
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
return $database;
|
||||
};
|
||||
}, ['cache', 'register', 'project', 'authorization']);
|
||||
|
||||
Server::setResource('abuseRetention', function () {
|
||||
return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day
|
||||
});
|
||||
|
||||
Server::setResource('auditRetention', function (Document $project) {
|
||||
if ($project->getId() === 'console') {
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
|
||||
}
|
||||
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
|
||||
}, ['project']);
|
||||
|
||||
Server::setResource('executionRetention', function () {
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
|
||||
});
|
||||
|
||||
Server::setResource('cache', function (Registry $register) {
|
||||
$pools = $register->get('pools');
|
||||
$list = Config::getParam('pools-cache', []);
|
||||
$adapters = [];
|
||||
|
||||
foreach ($list as $value) {
|
||||
$adapters[] = new CachePool($pools->get($value));
|
||||
}
|
||||
|
||||
return new Cache(new Sharding($adapters));
|
||||
}, ['register']);
|
||||
|
||||
Server::setResource('redis', function () {
|
||||
$host = System::getEnv('_APP_REDIS_HOST', 'localhost');
|
||||
$port = System::getEnv('_APP_REDIS_PORT', 6379);
|
||||
$pass = System::getEnv('_APP_REDIS_PASS', '');
|
||||
|
||||
$redis = new \Redis();
|
||||
@$redis->pconnect($host, (int) $port);
|
||||
if ($pass) {
|
||||
$redis->auth($pass);
|
||||
}
|
||||
$redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
|
||||
|
||||
return $redis;
|
||||
});
|
||||
|
||||
Server::setResource('timelimit', function (\Redis $redis) {
|
||||
return function (string $key, int $limit, int $time) use ($redis) {
|
||||
return new TimeLimitRedis($key, $limit, $time, $redis);
|
||||
};
|
||||
}, ['redis']);
|
||||
|
||||
Server::setResource('log', fn () => new Log());
|
||||
|
||||
Server::setResource('publisher', function (Group $pools) {
|
||||
return new BrokerPool(publisher: $pools->get('publisher'));
|
||||
}, ['pools']);
|
||||
|
||||
Server::setResource('publisherDatabases', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('publisherFunctions', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('publisherMigrations', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('publisherMessaging', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('consumer', function (Group $pools) {
|
||||
$container->set('consumer', function (Group $pools) {
|
||||
return new BrokerPool(consumer: $pools->get('consumer'));
|
||||
}, ['pools']);
|
||||
|
||||
Server::setResource('consumerDatabases', function (BrokerPool $consumer) {
|
||||
$container->set('consumerDatabases', function (BrokerPool $consumer) {
|
||||
return $consumer;
|
||||
}, ['consumer']);
|
||||
|
||||
Server::setResource('consumerMigrations', function (BrokerPool $consumer) {
|
||||
$container->set('consumerMigrations', function (BrokerPool $consumer) {
|
||||
return $consumer;
|
||||
}, ['consumer']);
|
||||
|
||||
Server::setResource('consumerStatsUsage', function (BrokerPool $consumer) {
|
||||
$container->set('consumerStatsUsage', function (BrokerPool $consumer) {
|
||||
return $consumer;
|
||||
}, ['consumer']);
|
||||
|
||||
Server::setResource('usage', function () {
|
||||
return new Context();
|
||||
}, []);
|
||||
Server::setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
|
||||
Server::setResource('queueForDatabase', function (Publisher $publisher) {
|
||||
return new EventDatabase($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForMessaging', function (Publisher $publisher) {
|
||||
return new Messaging($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForMails', function (Publisher $publisher) {
|
||||
return new Mail($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForBuilds', function (Publisher $publisher) {
|
||||
return new Build($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForScreenshots', function (Publisher $publisher) {
|
||||
return new Screenshot($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForDeletes', function (Publisher $publisher) {
|
||||
return new Delete($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForEvents', function (Publisher $publisher) {
|
||||
return new Event($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForAudits', function (Publisher $publisher) {
|
||||
return new Audit($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForWebhooks', function (Publisher $publisher) {
|
||||
return new Webhook($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForFunctions', function (Publisher $publisher) {
|
||||
return new Func($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForRealtime', function () {
|
||||
return new Realtime();
|
||||
}, []);
|
||||
|
||||
Server::setResource('queueForCertificates', function (Publisher $publisher) {
|
||||
return new Certificate($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForMigrations', function (Publisher $publisher) {
|
||||
return new Migration($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('logger', function (Registry $register) {
|
||||
return $register->get('logger');
|
||||
}, ['register']);
|
||||
|
||||
Server::setResource('pools', function (Registry $register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
|
||||
Server::setResource('telemetry', fn () => new NoTelemetry());
|
||||
|
||||
Server::setResource('deviceForSites', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
Server::setResource('deviceForMigrations', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
Server::setResource('deviceForFunctions', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
Server::setResource('deviceForFiles', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
Server::setResource('deviceForBuilds', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
Server::setResource('deviceForCache', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
Server::setResource(
|
||||
'isResourceBlocked',
|
||||
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
|
||||
);
|
||||
|
||||
Server::setResource('plan', function (array $plan = []) {
|
||||
return [];
|
||||
});
|
||||
|
||||
Server::setResource('certificates', function () {
|
||||
$container->set('certificates', function () {
|
||||
$email = System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'));
|
||||
if (empty($email)) {
|
||||
throw new Exception('You must set a valid security email address (_APP_EMAIL_CERTIFICATES) to issue a LetsEncrypt SSL certificate.');
|
||||
}
|
||||
|
||||
return new LetsEncrypt($email);
|
||||
});
|
||||
}, []);
|
||||
|
||||
Server::setResource('logError', function (Registry $register, Document $project) {
|
||||
return function (Throwable $error, string $namespace, string $action, ?array $extras = null) use ($register, $project) {
|
||||
$logger = $register->get('logger');
|
||||
|
||||
if ($logger) {
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
$log = new Log();
|
||||
$log->setNamespace($namespace);
|
||||
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
$log->addTag('projectId', $project->getId() ?? '');
|
||||
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
|
||||
if ($error->getPrevious() !== null) {
|
||||
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
|
||||
$log->addExtra('previousMessage', $error->getPrevious()->getMessage());
|
||||
}
|
||||
$log->addExtra('previousFile', $error->getPrevious()->getFile());
|
||||
$log->addExtra('previousLine', $error->getPrevious()->getLine());
|
||||
}
|
||||
|
||||
foreach (($extras ?? []) as $key => $value) {
|
||||
$log->addExtra($key, $value);
|
||||
}
|
||||
|
||||
$log->setAction($action);
|
||||
|
||||
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
try {
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Error log pushed with status code: ' . $responseCode);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Error pushing log: ' . $th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
Console::warning("Failed: {$error->getMessage()}");
|
||||
Console::warning($error->getTraceAsString());
|
||||
|
||||
if ($error->getPrevious() !== null) {
|
||||
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
|
||||
Console::warning("Previous Failed: {$error->getPrevious()->getMessage()}");
|
||||
}
|
||||
Console::warning("Previous File: {$error->getPrevious()->getFile()} Line: {$error->getPrevious()->getLine()}");
|
||||
}
|
||||
};
|
||||
}, ['register', 'project']);
|
||||
|
||||
Server::setResource('executor', fn () => new Executor());
|
||||
|
||||
Server::setResource('getAudit', function (Database $dbForPlatform, callable $getProjectDB) {
|
||||
return function (Document $project) use ($dbForPlatform, $getProjectDB) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
$adapter = new AdapterDatabase($dbForPlatform);
|
||||
|
||||
return new UtopiaAudit($adapter);
|
||||
}
|
||||
|
||||
$dbForProject = $getProjectDB($project);
|
||||
$adapter = new AdapterDatabase($dbForProject);
|
||||
|
||||
return new UtopiaAudit($adapter);
|
||||
};
|
||||
}, ['dbForPlatform', 'getProjectDB']);
|
||||
|
||||
Server::setResource('executionsRetentionCount', function (Document $project, array $plan) {
|
||||
if ($project->getId() === 'console' || empty($plan)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) ($plan['executionsRetentionCount'] ?? 100);
|
||||
}, ['project', 'plan']);
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$platform = new Appwrite();
|
||||
$args = $platform->getEnv('argv');
|
||||
$args = $_SERVER['argv'] ?? [];
|
||||
|
||||
if (! isset($args[1])) {
|
||||
Console::error('Missing worker name');
|
||||
@@ -575,38 +79,45 @@ if (\str_starts_with($workerName, 'databases')) {
|
||||
$queueName = System::getEnv('_APP_QUEUE_NAME', 'v1-' . strtolower($workerName));
|
||||
}
|
||||
|
||||
/** @var \Utopia\Pools\Group $pools */
|
||||
$pools = $container->get('pools');
|
||||
|
||||
$adapter = new Swoole(
|
||||
$pools->get('consumer')->pop()->getResource(),
|
||||
System::getEnv('_APP_WORKERS_NUM', 1),
|
||||
$queueName
|
||||
);
|
||||
|
||||
$worker = new Server($adapter, $container);
|
||||
|
||||
try {
|
||||
/**
|
||||
* Any worker can be configured with the following env vars:
|
||||
* - _APP_WORKERS_NUM The total number of worker processes
|
||||
* - _APP_WORKER_PER_CORE The number of worker processes per core (ignored if _APP_WORKERS_NUM is set)
|
||||
* - _APP_QUEUE_NAME The name of the queue to read for database events
|
||||
*/
|
||||
$worker->init()->action(function () use ($worker, $registerWorkerMessageResources) {
|
||||
$registerWorkerMessageResources($worker->getContainer());
|
||||
});
|
||||
|
||||
$container->set('bus', function ($register) use ($worker) {
|
||||
return $register->get('bus')->setResolver(
|
||||
fn (string $name) => $worker->getContainer()->get($name)
|
||||
);
|
||||
}, ['register']);
|
||||
|
||||
$platform->setWorker($worker);
|
||||
$platform->init(Service::TYPE_WORKER, [
|
||||
'workersNum' => System::getEnv('_APP_WORKERS_NUM', 1),
|
||||
'connection' => $pools->get('consumer')->pop()->getResource(),
|
||||
'workerName' => strtolower($workerName) ?? null,
|
||||
'queueName' => $queueName,
|
||||
'workerName' => strtolower($workerName),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine());
|
||||
Console::exit(1);
|
||||
}
|
||||
|
||||
$worker = $platform->getWorker();
|
||||
|
||||
Server::setResource('bus', function ($register) use ($worker) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $worker->getResource($name));
|
||||
}, ['register']);
|
||||
|
||||
$worker
|
||||
->error()
|
||||
->inject('error')
|
||||
->inject('logger')
|
||||
->inject('log')
|
||||
->inject('pools')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project, Authorization $authorization) use ($queueName) {
|
||||
->action(function (Throwable $error, ?Logger $logger, Log $log, Document $project, Authorization $authorization) use ($queueName) {
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
if ($logger) {
|
||||
|
||||
+9
-10
@@ -52,34 +52,34 @@
|
||||
"appwrite/php-runtimes": "0.19.*",
|
||||
"appwrite/php-clamav": "2.0.*",
|
||||
"utopia-php/abuse": "1.2.*",
|
||||
"utopia-php/agents": "1.2.*",
|
||||
"utopia-php/analytics": "0.15.*",
|
||||
"utopia-php/audit": "2.2.*",
|
||||
"utopia-php/auth": "0.5.*",
|
||||
"utopia-php/cache": "1.0.*",
|
||||
"utopia-php/cli": "0.22.*",
|
||||
"utopia-php/cli": "0.23.*",
|
||||
"utopia-php/compression": "0.1.*",
|
||||
"utopia-php/config": "1.*",
|
||||
"utopia-php/console": "0.1.*",
|
||||
"utopia-php/database": "5.*",
|
||||
"utopia-php/agents": "1.*",
|
||||
"utopia-php/detector": "0.2.*",
|
||||
"utopia-php/domains": "1.*",
|
||||
"utopia-php/emails": "0.6.*",
|
||||
"utopia-php/dns": "1.6.*",
|
||||
"utopia-php/dsn": "0.2.1",
|
||||
"utopia-php/framework": "0.33.*",
|
||||
"utopia-php/http": "0.34.*",
|
||||
"utopia-php/fetch": "0.5.*",
|
||||
"utopia-php/image": "0.8.*",
|
||||
"utopia-php/locale": "0.8.*",
|
||||
"utopia-php/logger": "0.6.*",
|
||||
"utopia-php/messaging": "0.20.*",
|
||||
"utopia-php/messaging": "0.22.*",
|
||||
"utopia-php/migration": "1.9.*",
|
||||
"utopia-php/platform": "0.7.*",
|
||||
"utopia-php/platform": "0.12.*",
|
||||
"utopia-php/pools": "1.*",
|
||||
"utopia-php/span": "1.1.*",
|
||||
"utopia-php/preloader": "0.2.*",
|
||||
"utopia-php/queue": "0.15.*",
|
||||
"utopia-php/servers": "0.2.5",
|
||||
"utopia-php/queue": "0.17.*",
|
||||
"utopia-php/servers": "0.3.*",
|
||||
"utopia-php/registry": "0.5.*",
|
||||
"utopia-php/storage": "1.0.*",
|
||||
"utopia-php/system": "0.10.*",
|
||||
@@ -92,10 +92,9 @@
|
||||
"chillerlan/php-qrcode": "4.3.*",
|
||||
"adhocore/jwt": "1.1.*",
|
||||
"spomky-labs/otphp": "11.*",
|
||||
"webonyx/graphql-php": "14.11.*",
|
||||
"webonyx/graphql-php": "15.31.*",
|
||||
"league/csv": "9.14.*",
|
||||
"enshrined/svg-sanitize": "0.22.*",
|
||||
"utopia-php/di": "0.1.0"
|
||||
"enshrined/svg-sanitize": "0.22.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-fileinfo": "*",
|
||||
|
||||
Generated
+230
-223
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
Export documents to a JSON file from your Appwrite database. This endpoint allows you to export documents to a JSON file stored in a secure internal bucket. You'll receive an email with a download link when the export is complete.
|
||||
@@ -0,0 +1 @@
|
||||
Import documents from a JSON file into your Appwrite database. This endpoint allows you to import documents from a JSON file uploaded to Appwrite Storage bucket.
|
||||
@@ -1,721 +0,0 @@
|
||||
parameters:
|
||||
ignoreErrors:
|
||||
-
|
||||
message: '#^PHPDoc tag @var above a method has no effect\.$#'
|
||||
identifier: varTag.misplaced
|
||||
count: 1
|
||||
path: app/config/templates/function.php
|
||||
|
||||
-
|
||||
message: '#^Array has 2 duplicate keys with value ''outputDirectory'' \(''outputDirectory'', ''outputDirectory''\)\.$#'
|
||||
identifier: array.duplicateKey
|
||||
count: 3
|
||||
path: app/config/templates/site.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @var above a method has no effect\.$#'
|
||||
identifier: varTag.misplaced
|
||||
count: 1
|
||||
path: app/config/templates/site.php
|
||||
|
||||
-
|
||||
message: '#^Result of method Appwrite\\Utopia\\Response\:\:dynamic\(\) \(void\) is used\.$#'
|
||||
identifier: method.void
|
||||
count: 2
|
||||
path: app/controllers/api/account.php
|
||||
|
||||
-
|
||||
message: '#^Result of method Utopia\\Http\\Response\:\:noContent\(\) \(void\) is used\.$#'
|
||||
identifier: method.void
|
||||
count: 1
|
||||
path: app/controllers/api/account.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$session might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 3
|
||||
path: app/controllers/api/account.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$output might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 1
|
||||
path: app/controllers/api/locale.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$currentScheduledAt on left side of \?\? is never defined\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: app/controllers/api/messaging.php
|
||||
|
||||
-
|
||||
message: '#^Result of method Utopia\\Http\\Response\:\:noContent\(\) \(void\) is used\.$#'
|
||||
identifier: method.void
|
||||
count: 1
|
||||
path: app/controllers/api/projects.php
|
||||
|
||||
-
|
||||
message: '#^Result of method Appwrite\\Utopia\\Response\:\:dynamic\(\) \(void\) is used\.$#'
|
||||
identifier: method.void
|
||||
count: 2
|
||||
path: app/controllers/api/users.php
|
||||
|
||||
-
|
||||
message: '#^Result of method Utopia\\Http\\Response\:\:noContent\(\) \(void\) is used\.$#'
|
||||
identifier: method.void
|
||||
count: 1
|
||||
path: app/controllers/api/users.php
|
||||
|
||||
-
|
||||
message: '#^Result of method Utopia\\Http\\Response\:\:redirect\(\) \(void\) is used\.$#'
|
||||
identifier: method.void
|
||||
count: 2
|
||||
path: app/controllers/general.php
|
||||
|
||||
-
|
||||
message: '#^Result of method Utopia\\Http\\Response\:\:send\(\) \(void\) is used\.$#'
|
||||
identifier: method.void
|
||||
count: 1
|
||||
path: app/controllers/general.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$body on left side of \?\? always exists and is not nullable\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: app/controllers/general.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$deployment in PHPDoc tag @var does not exist\.$#'
|
||||
identifier: varTag.variableNotFound
|
||||
count: 1
|
||||
path: app/controllers/general.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$executionId on left side of \?\? always exists and is not nullable\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: app/controllers/general.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$executionResponse might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 1
|
||||
path: app/controllers/general.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$user might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 1
|
||||
path: app/controllers/general.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$installation might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 1
|
||||
path: app/controllers/mock.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$register might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 3
|
||||
path: app/http.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$tag on left side of \?\? always exists and is always null\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: app/init/database/filters.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$dsn\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 1
|
||||
path: app/init/registers.php
|
||||
|
||||
-
|
||||
message: '#^Binary operation "/" between string and string results in an error\.$#'
|
||||
identifier: binaryOp.invalid
|
||||
count: 1
|
||||
path: app/init/registers.php
|
||||
|
||||
-
|
||||
message: '#^Property PHPMailer\\PHPMailer\\PHPMailer\:\:\$Port \(int\) does not accept string\.$#'
|
||||
identifier: assign.propertyType
|
||||
count: 1
|
||||
path: app/init/registers.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$providerConfig in empty\(\) always exists and is not falsy\.$#'
|
||||
identifier: empty.variable
|
||||
count: 2
|
||||
path: app/init/registers.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$providerConfig on left side of \?\? always exists and is not nullable\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: app/init/registers.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$hostname on left side of \?\? always exists and is not nullable\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: app/init/resources.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$register\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 4
|
||||
path: app/realtime.php
|
||||
|
||||
-
|
||||
message: '#^Class Utopia\\Database\\Validator\\Authorization does not have a constructor and must be instantiated without any parameters\.$#'
|
||||
identifier: new.noConstructor
|
||||
count: 1
|
||||
path: app/realtime.php
|
||||
|
||||
-
|
||||
message: '#^Function triggerStats\(\) returns void but does not have any side effects\.$#'
|
||||
identifier: void.pure
|
||||
count: 1
|
||||
path: app/realtime.php
|
||||
|
||||
-
|
||||
message: '#^Binary operation "\*" between \-1 and string results in an error\.$#'
|
||||
identifier: binaryOp.invalid
|
||||
count: 3
|
||||
path: app/worker.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$context\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 1
|
||||
path: src/Appwrite/GraphQL/Resolvers.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$info\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 1
|
||||
path: src/Appwrite/GraphQL/Resolvers.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$type\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 5
|
||||
path: src/Appwrite/GraphQL/Resolvers.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$request in PHPDoc tag @var does not exist\.$#'
|
||||
identifier: varTag.variableNotFound
|
||||
count: 1
|
||||
path: src/Appwrite/GraphQL/Resolvers.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$response in PHPDoc tag @var does not exist\.$#'
|
||||
identifier: varTag.variableNotFound
|
||||
count: 1
|
||||
path: src/Appwrite/GraphQL/Resolvers.php
|
||||
|
||||
-
|
||||
message: '#^Method Appwrite\\Network\\Cors\:\:headers\(\) should return array\<string, string\> but returns array\<string, int\|string\>\.$#'
|
||||
identifier: return.type
|
||||
count: 5
|
||||
path: src/Appwrite/Network/Cors.php
|
||||
|
||||
-
|
||||
message: '#^Parameter &\$tag by\-ref type of method Appwrite\\OpenSSL\\OpenSSL\:\:encrypt\(\) expects null, string\|null given\.$#'
|
||||
identifier: parameterByRef.type
|
||||
count: 1
|
||||
path: src/Appwrite/OpenSSL/OpenSSL.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$output in empty\(\) always exists and is not falsy\.$#'
|
||||
identifier: empty.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Avatars/Http/Action.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$output in empty\(\) always exists and is not falsy\.$#'
|
||||
identifier: empty.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Avatars/Http/Favicon/Get.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$output in empty\(\) always exists and is not falsy\.$#'
|
||||
identifier: empty.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php
|
||||
|
||||
-
|
||||
message: '#^Binary operation "%%" between string and 5 results in an error\.$#'
|
||||
identifier: binaryOp.invalid
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php
|
||||
|
||||
-
|
||||
message: '#^Method Appwrite\\Platform\\Modules\\Databases\\Http\\Databases\\Action\:\:setHttpPath\(\) should return Appwrite\\Platform\\Action but returns Utopia\\Platform\\Action\.$#'
|
||||
identifier: return.type
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Action.php
|
||||
|
||||
-
|
||||
message: '#^Method Appwrite\\Platform\\Modules\\Databases\\Http\\Databases\\Collections\\Documents\\Action\:\:setHttpPath\(\) should return Appwrite\\Platform\\Modules\\Databases\\Http\\Databases\\Action but returns Appwrite\\Platform\\Action\.$#'
|
||||
identifier: return.type
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$relations in empty\(\) always exists and is not falsy\.$#'
|
||||
identifier: empty.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$dbForProject\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$document in PHPDoc tag @var does not match assigned variable \$collectionTableId\.$#'
|
||||
identifier: varTag.differentVariable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$hostname on left side of \?\? always exists and is not nullable\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$enabled on left side of \?\?\= always exists and is not nullable\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php
|
||||
|
||||
-
|
||||
message: '#^Offset ''deviceBrand'' does not exist on int\.$#'
|
||||
identifier: offsetAccess.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php
|
||||
|
||||
-
|
||||
message: '#^Offset ''deviceModel'' does not exist on int\.$#'
|
||||
identifier: offsetAccess.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php
|
||||
|
||||
-
|
||||
message: '#^Offset ''deviceName'' does not exist on int\.$#'
|
||||
identifier: offsetAccess.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php
|
||||
|
||||
-
|
||||
message: '#^Method Appwrite\\Platform\\Modules\\Databases\\Http\\Databases\\Transactions\\Action\:\:setHttpPath\(\) should return Appwrite\\Platform\\Modules\\Databases\\Http\\Databases\\Action but returns Appwrite\\Platform\\Action\.$#'
|
||||
identifier: return.type
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$existing\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$queueForEvents\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$queueForFunctions\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$queueForRealtime\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$queueForWebhooks\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$usage\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @throws with type Appwrite\\Platform\\Modules\\Databases\\Http\\Databases\\Transactions\\Structure\|Throwable\|Utopia\\Database\\Validator\\Authorization is not subtype of Throwable$#'
|
||||
identifier: throws.notThrowable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
|
||||
|
||||
-
|
||||
message: '#^Offset ''deviceBrand'' does not exist on int\.$#'
|
||||
identifier: offsetAccess.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php
|
||||
|
||||
-
|
||||
message: '#^Offset ''deviceModel'' does not exist on int\.$#'
|
||||
identifier: offsetAccess.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php
|
||||
|
||||
-
|
||||
message: '#^Offset ''deviceName'' does not exist on int\.$#'
|
||||
identifier: offsetAccess.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php
|
||||
|
||||
-
|
||||
message: '#^Call to method getAttribute\(\) on an unknown class Appwrite\\Platform\\Modules\\Functions\\Http\\Executions\\Utopia\\Database\\Document\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
|
||||
|
||||
-
|
||||
message: '#^Call to method getId\(\) on an unknown class Appwrite\\Platform\\Modules\\Functions\\Http\\Executions\\Utopia\\Database\\Document\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
|
||||
|
||||
-
|
||||
message: '#^Call to method isEmpty\(\) on an unknown class Appwrite\\Platform\\Modules\\Functions\\Http\\Executions\\Utopia\\Database\\Document\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
|
||||
|
||||
-
|
||||
message: '#^Method Appwrite\\Platform\\Modules\\Functions\\Http\\Executions\\Create\:\:enqueueDeletes\(\) returns void but does not have any side effects\.$#'
|
||||
identifier: void.pure
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @var for variable \$session contains unknown class Appwrite\\Platform\\Modules\\Functions\\Http\\Executions\\Utopia\\Database\\Document\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
|
||||
|
||||
-
|
||||
message: '#^Result of method Appwrite\\Utopia\\Response\:\:dynamic\(\) \(void\) is used\.$#'
|
||||
identifier: method.void
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$body on left side of \?\? always exists and is not nullable\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$executionId on left side of \?\? always exists and is not nullable\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$jwt on left side of \?\? always exists and is not nullable\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Appwrite\\Event\\Event\:\:setSubscribers\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php
|
||||
|
||||
-
|
||||
message: '#^Undefined variable\: \$cpus$#'
|
||||
identifier: variable.undefined
|
||||
count: 3
|
||||
path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
|
||||
|
||||
-
|
||||
message: '#^Undefined variable\: \$memory$#'
|
||||
identifier: variable.undefined
|
||||
count: 3
|
||||
path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$deployment might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 4
|
||||
path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$logsAfter on left side of \?\? always exists and is not nullable\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$logsBefore on left side of \?\? always exists and is not nullable\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$providerCommitHash on left side of \?\? always exists and is not nullable\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$rule in empty\(\) always exists and is not falsy\.$#'
|
||||
identifier: empty.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
|
||||
|
||||
-
|
||||
message: '#^Result of method Appwrite\\Utopia\\Response\:\:dynamic\(\) \(void\) is used\.$#'
|
||||
identifier: method.void
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$iv might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$tag might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php
|
||||
|
||||
-
|
||||
message: '#^Caught class Appwrite\\Platform\\Modules\\Teams\\Http\\Memberships\\Throwable not found\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$email in empty\(\) always exists and is always falsy\.$#'
|
||||
identifier: empty.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$hash might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$invitee might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 14
|
||||
path: src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$providerConfig on left side of \?\? always exists and is not nullable\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Tasks/Doctor.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$compose in isset\(\) always exists and is not nullable\.$#'
|
||||
identifier: isset.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Tasks/Install.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$prUrls might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Tasks/SDKs.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$dbForPlatform\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Tasks/StatsResources.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param references unknown parameter\: \$getProjectDB$#'
|
||||
identifier: parameter.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Workers/Audits.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$certificates\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 2
|
||||
path: src/Appwrite/Platform/Workers/Deletes.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$dbForPlatform\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Workers/Deletes.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$dbForProject\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Workers/Deletes.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$project\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 6
|
||||
path: src/Appwrite/Platform/Workers/Deletes.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$resourceType\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Workers/Deletes.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param references unknown parameter\: \$build$#'
|
||||
identifier: parameter.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Workers/Deletes.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param references unknown parameter\: \$dbForPlatform$#'
|
||||
identifier: parameter.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Workers/Deletes.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param references unknown parameter\: \$target$#'
|
||||
identifier: parameter.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Workers/Deletes.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @throws with type Appwrite\\Extend\\Exception\|Utopia\\Database\\Exception\|Utopia\\Database\\Validator\\Authorization is not subtype of Throwable$#'
|
||||
identifier: throws.notThrowable
|
||||
count: 2
|
||||
path: src/Appwrite/Platform/Workers/Deletes.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @throws with type Appwrite\\Extend\\Exception\|Utopia\\Database\\Validator\\Authorization is not subtype of Throwable$#'
|
||||
identifier: throws.notThrowable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Workers/Deletes.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @throws with type Appwrite\\Platform\\Workers\\Exception is not subtype of Throwable$#'
|
||||
identifier: throws.notThrowable
|
||||
count: 2
|
||||
path: src/Appwrite/Platform/Workers/Functions.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$body on left side of \?\? always exists and is not nullable\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 2
|
||||
path: src/Appwrite/Platform/Workers/Functions.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$errorCode might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Workers/Functions.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$executionId on left side of \?\? always exists and is not nullable\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Workers/Functions.php
|
||||
|
||||
-
|
||||
message: '#^Binary operation "\+\=" between 0 and array\<array\<mixed\>\|int\|string\>\|int\|string results in an error\.$#'
|
||||
identifier: assignOp.invalid
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Workers/Messaging.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$provider might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Workers/Messaging.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$aggregatedResources might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Workers/Migrations.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$dbForLogs\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Workers/StatsResources.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$sequence\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Workers/StatsUsage.php
|
||||
|
||||
-
|
||||
message: '#^Callable callable\(\)\: Utopia\\Database\\Database invoked with 1 parameter, 0 required\.$#'
|
||||
identifier: arguments.count
|
||||
count: 2
|
||||
path: src/Appwrite/Platform/Workers/StatsUsage.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$curlError on left side of \?\? is never defined\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Workers/Webhooks.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe usage of new static\(\)\.$#'
|
||||
identifier: new.static
|
||||
count: 3
|
||||
path: src/Appwrite/Promises/Promise.php
|
||||
|
||||
-
|
||||
message: '#^Property Appwrite\\SDK\\Method\:\:\$hide on left side of \?\? is not nullable nor uninitialized\.$#'
|
||||
identifier: nullCoalesce.initializedProperty
|
||||
count: 1
|
||||
path: src/Appwrite/SDK/Method.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param has invalid value \(Document \$this\)\: Unexpected token "\$this", expected variable at offset 69 on line 4$#'
|
||||
identifier: phpDoc.parseError
|
||||
count: 1
|
||||
path: src/Appwrite/Utopia/Database/Documents/User.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param references unknown parameter\: \$sessions$#'
|
||||
identifier: parameter.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Utopia/Database/Documents/User.php
|
||||
|
||||
-
|
||||
message: '#^Binary operation "\+" between string and 1 results in an error\.$#'
|
||||
identifier: binaryOp.invalid
|
||||
count: 1
|
||||
path: tests/e2e/Services/GraphQL/Legacy/AbuseTest.php
|
||||
|
||||
-
|
||||
message: '#^Binary operation "\+" between string and 1 results in an error\.$#'
|
||||
identifier: binaryOp.invalid
|
||||
count: 1
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/AbuseTest.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Utopia\\Queue\\Publisher\:\:getEvents\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
count: 1
|
||||
path: tests/unit/Event/EventTest.php
|
||||
@@ -1,6 +1,3 @@
|
||||
includes:
|
||||
- phpstan-baseline.neon
|
||||
|
||||
parameters:
|
||||
level: 3
|
||||
tmpDir: .phpstan-cache
|
||||
@@ -15,4 +12,3 @@ parameters:
|
||||
- vendor/swoole/ide-helper
|
||||
excludePaths:
|
||||
- tests/resources
|
||||
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Auth\OAuth2;
|
||||
|
||||
use Appwrite\Auth\OAuth2;
|
||||
use Appwrite\OpenSSL\OpenSSL;
|
||||
use Utopia\System\System;
|
||||
|
||||
// Reference Material
|
||||
// https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code
|
||||
// https://docs.x.com/x-api/users/get-me
|
||||
|
||||
class X extends OAuth2
|
||||
{
|
||||
private const PKCE_STATE_KEY = '_pkce';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $user = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $tokens = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $scopes = [
|
||||
'tweet.read',
|
||||
'users.read',
|
||||
'users.email',
|
||||
'offline.access',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private string $pkceVerifier = '';
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'x';
|
||||
}
|
||||
|
||||
public function getLoginURL(): string
|
||||
{
|
||||
$state = $this->state;
|
||||
$state[self::PKCE_STATE_KEY] = $this->encryptPKCEVerifier($this->getPKCEVerifier());
|
||||
|
||||
return 'https://x.com/i/oauth2/authorize?' . \http_build_query([
|
||||
'response_type' => 'code',
|
||||
'client_id' => $this->appID,
|
||||
'redirect_uri' => $this->callback,
|
||||
'scope' => \implode(' ', $this->getScopes()),
|
||||
'state' => $this->base64UrlEncode(\json_encode($state, JSON_THROW_ON_ERROR)),
|
||||
'code_challenge' => $this->getPKCEChallenge(),
|
||||
'code_challenge_method' => 'S256',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $code
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getTokens(string $code): array
|
||||
{
|
||||
if (empty($this->tokens)) {
|
||||
$this->tokens = $this->decodeJsonObject($this->request(
|
||||
'POST',
|
||||
'https://api.x.com/2/oauth2/token',
|
||||
$this->tokenEndpointHeaders(),
|
||||
\http_build_query([
|
||||
'code' => $code,
|
||||
'client_id' => $this->appID,
|
||||
'grant_type' => 'authorization_code',
|
||||
'redirect_uri' => $this->callback,
|
||||
'code_verifier' => $this->getPKCEVerifier(),
|
||||
])
|
||||
));
|
||||
}
|
||||
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $refreshToken
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function refreshTokens(string $refreshToken): array
|
||||
{
|
||||
$this->tokens = $this->decodeJsonObject($this->request(
|
||||
'POST',
|
||||
'https://api.x.com/2/oauth2/token',
|
||||
$this->tokenEndpointHeaders(),
|
||||
\http_build_query([
|
||||
'client_id' => $this->appID,
|
||||
'refresh_token' => $refreshToken,
|
||||
'grant_type' => 'refresh_token',
|
||||
])
|
||||
));
|
||||
|
||||
if (empty($this->tokens['refresh_token'])) {
|
||||
$this->tokens['refresh_token'] = $refreshToken;
|
||||
}
|
||||
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserID(string $accessToken): string
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
return $user['data']['id'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserEmail(string $accessToken): string
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
return $user['data']['confirmed_email'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the OAuth email is verified.
|
||||
*
|
||||
* X returns a confirmed email only when the app has email access enabled
|
||||
* and the authenticated user has a confirmed email address.
|
||||
*
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEmailVerified(string $accessToken): bool
|
||||
{
|
||||
return !empty($this->getUserEmail($accessToken));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserName(string $accessToken): string
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
return $user['data']['name'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getUser(string $accessToken): array
|
||||
{
|
||||
if (empty($this->user)) {
|
||||
$this->user = $this->decodeJsonObject($this->request(
|
||||
'GET',
|
||||
'https://api.x.com/2/users/me?user.fields=confirmed_email',
|
||||
['Authorization: Bearer ' . $accessToken]
|
||||
));
|
||||
}
|
||||
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function parseState(string $state): ?array
|
||||
{
|
||||
$decoded = $this->base64UrlDecode($state);
|
||||
if ($decoded === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parsed = \json_decode($decoded, true);
|
||||
|
||||
if (!\is_array($parsed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$pkce = $parsed[self::PKCE_STATE_KEY] ?? null;
|
||||
|
||||
if (\is_array($pkce)) {
|
||||
$this->pkceVerifier = $this->decryptPKCEVerifier($pkce);
|
||||
}
|
||||
|
||||
unset($parsed[self::PKCE_STATE_KEY]);
|
||||
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function tokenEndpointHeaders(): array
|
||||
{
|
||||
return [
|
||||
'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret),
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function decodeJsonObject(string $json): array
|
||||
{
|
||||
$decoded = \json_decode($json, true);
|
||||
|
||||
return \is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
private function getPKCEVerifier(): string
|
||||
{
|
||||
if ($this->pkceVerifier === '') {
|
||||
$this->pkceVerifier = $this->base64UrlEncode(\random_bytes(64));
|
||||
}
|
||||
|
||||
return $this->pkceVerifier;
|
||||
}
|
||||
|
||||
private function getPKCEChallenge(): string
|
||||
{
|
||||
return $this->base64UrlEncode(\hash('sha256', $this->getPKCEVerifier(), true));
|
||||
}
|
||||
|
||||
private function encryptPKCEVerifier(string $verifier): array
|
||||
{
|
||||
$iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM));
|
||||
$key = $this->getPKCEStateKey();
|
||||
$tag = null;
|
||||
|
||||
$data = OpenSSL::encrypt($verifier, OpenSSL::CIPHER_AES_128_GCM, $key, OPENSSL_RAW_DATA, $iv, $tag);
|
||||
|
||||
if ($data === false || $tag === null) {
|
||||
throw new \Exception('Failed to encrypt PKCE verifier.');
|
||||
}
|
||||
|
||||
return [
|
||||
'data' => $this->base64UrlEncode($data),
|
||||
'iv' => \bin2hex($iv),
|
||||
'tag' => \bin2hex($tag),
|
||||
];
|
||||
}
|
||||
|
||||
private function decryptPKCEVerifier(array $payload): string
|
||||
{
|
||||
$data = $payload['data'] ?? '';
|
||||
$iv = $payload['iv'] ?? '';
|
||||
$tag = $payload['tag'] ?? '';
|
||||
|
||||
if ($data === '' || $iv === '' || $tag === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$decodedData = $this->base64UrlDecode($data);
|
||||
$decodedIv = \hex2bin($iv);
|
||||
$decodedTag = \hex2bin($tag);
|
||||
|
||||
if ($decodedData === false || $decodedIv === false || $decodedTag === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return OpenSSL::decrypt(
|
||||
$decodedData,
|
||||
OpenSSL::CIPHER_AES_128_GCM,
|
||||
$this->getPKCEStateKey(),
|
||||
OPENSSL_RAW_DATA,
|
||||
$decodedIv,
|
||||
$decodedTag
|
||||
) ?: '';
|
||||
}
|
||||
|
||||
private function getPKCEStateKey(): string
|
||||
{
|
||||
$key = System::getEnv('_APP_OPENSSL_KEY_V1', '');
|
||||
|
||||
if ($key === '') {
|
||||
throw new \Exception('X OAuth2 requires _APP_OPENSSL_KEY_V1 to encrypt PKCE state.');
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
private function base64UrlEncode(string $value): string
|
||||
{
|
||||
return \rtrim(\strtr(\base64_encode($value), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
private function base64UrlDecode(string $value): string|false
|
||||
{
|
||||
$padding = \strlen($value) % 4;
|
||||
if ($padding > 0) {
|
||||
$value .= \str_repeat('=', 4 - $padding);
|
||||
}
|
||||
|
||||
return \base64_decode(\strtr($value, '-_', '+/'), true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,8 +23,9 @@ class Yahoo extends OAuth2
|
||||
* @var array
|
||||
*/
|
||||
protected array $scopes = [
|
||||
'sdct-r',
|
||||
'sdpp-w',
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Bus\Events;
|
||||
|
||||
use Utopia\Bus\Event;
|
||||
|
||||
class SessionCreated implements Event
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $user
|
||||
* @param array<string, mixed> $project
|
||||
* @param array<string, mixed> $session
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly array $user,
|
||||
public readonly array $project,
|
||||
public readonly array $session,
|
||||
public readonly string $locale,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,12 @@
|
||||
namespace Appwrite\Bus\Listeners;
|
||||
|
||||
use Appwrite\Bus\Events\ExecutionCompleted;
|
||||
use Appwrite\Event\Execution;
|
||||
use Appwrite\Event\Message\Execution as ExecutionMessage;
|
||||
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
|
||||
use Utopia\Bus\Listener;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Span\Span;
|
||||
use Utopia\System\System;
|
||||
|
||||
class Log extends Listener
|
||||
{
|
||||
@@ -24,16 +26,33 @@ class Log extends Listener
|
||||
{
|
||||
$this
|
||||
->desc('Persists execution logs to database via queue')
|
||||
->inject('publisher')
|
||||
->inject('publisherForExecutions')
|
||||
->callback($this->handle(...));
|
||||
}
|
||||
|
||||
public function handle(ExecutionCompleted $event, Publisher $publisher): void
|
||||
public function handle(ExecutionCompleted $event, ExecutionPublisher $publisherForExecutions): void
|
||||
{
|
||||
$queueForExecutions = new Execution($publisher);
|
||||
$queueForExecutions
|
||||
->setExecution(new Document($event->execution))
|
||||
->setProject(new Document($event->project))
|
||||
->trigger();
|
||||
$project = new Document($event->project);
|
||||
$execution = new Document($event->execution);
|
||||
if ($execution->getAttribute('resourceType', '') === 'functions') {
|
||||
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
|
||||
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
|
||||
$resourceId = $execution->getAttribute('resourceId', '');
|
||||
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $resourceId === $traceFunctionId) {
|
||||
Span::init('execution.trace.v1_executions_enqueue');
|
||||
Span::add('datetime', gmdate('c'));
|
||||
Span::add('projectId', $project->getId());
|
||||
Span::add('functionId', $resourceId);
|
||||
Span::add('executionId', $execution->getId());
|
||||
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
|
||||
Span::add('status', $execution->getAttribute('status', ''));
|
||||
Span::current()?->finish();
|
||||
}
|
||||
}
|
||||
|
||||
$publisherForExecutions->enqueue(new ExecutionMessage(
|
||||
project: $project,
|
||||
execution: $execution,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Bus\Listeners;
|
||||
|
||||
use Appwrite\Auth\MFA\Type;
|
||||
use Appwrite\Bus\Events\SessionCreated;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Template\Template;
|
||||
use Utopia\Bus\Listener;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Storage\Validator\FileName;
|
||||
use Utopia\System\System;
|
||||
|
||||
class Mails extends Listener
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'mails';
|
||||
}
|
||||
|
||||
public static function getEvents(): array
|
||||
{
|
||||
return [SessionCreated::class];
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->desc('Sends session alert emails')
|
||||
->inject('publisher')
|
||||
->inject('locale')
|
||||
->inject('platform')
|
||||
->inject('dbForProject')
|
||||
->callback($this->handle(...));
|
||||
}
|
||||
|
||||
public function handle(SessionCreated $event, Publisher $publisher, Locale $locale, array $platform, Database $dbForProject): void
|
||||
{
|
||||
$project = new Document($event->project);
|
||||
|
||||
if (!($project->getAttribute('auths', [])['sessionAlerts'] ?? false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($event->user['email'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$provider = $event->session['provider'] ?? '';
|
||||
$factors = $event->session['factors'] ?? [];
|
||||
|
||||
if (\in_array($provider, [SESSION_PROVIDER_MAGIC_URL, SESSION_PROVIDER_TOKEN]) && \in_array(Type::EMAIL, $factors)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($dbForProject->count('sessions', [Query::equal('userId', [$event->user['$id']])]) === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$locale->setDefault($event->locale);
|
||||
|
||||
$session = new Document($event->session);
|
||||
$smtp = $project->getAttribute('smtp', []);
|
||||
$smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base');
|
||||
|
||||
if (!(new FileName())->isValid($smtpBaseTemplate)) {
|
||||
throw new \Exception('Invalid template path');
|
||||
}
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])["email.sessionAlert-$event->locale"] ?? [];
|
||||
$isBranded = $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE;
|
||||
|
||||
$subject = $customTemplate['subject'] ?? $locale->getText('emails.sessionAlert.subject');
|
||||
$preview = $locale->getText('emails.sessionAlert.preview');
|
||||
|
||||
$body = empty($customTemplate['message'])
|
||||
? Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-session-alert.tpl')
|
||||
->setParam('{{hello}}', $locale->getText('emails.sessionAlert.hello'))
|
||||
->setParam('{{body}}', $locale->getText('emails.sessionAlert.body'))
|
||||
->setParam('{{listDevice}}', $locale->getText('emails.sessionAlert.listDevice'))
|
||||
->setParam('{{listIpAddress}}', $locale->getText('emails.sessionAlert.listIpAddress'))
|
||||
->setParam('{{listCountry}}', $locale->getText('emails.sessionAlert.listCountry'))
|
||||
->setParam('{{footer}}', $locale->getText('emails.sessionAlert.footer'))
|
||||
->setParam('{{thanks}}', $locale->getText('emails.sessionAlert.thanks'))
|
||||
->setParam('{{signature}}', $locale->getText('emails.sessionAlert.signature'))
|
||||
->render()
|
||||
: $customTemplate['message'];
|
||||
|
||||
$clientName = $session->getAttribute('clientName')
|
||||
?: ($session->getAttribute('userAgent') ?: 'UNKNOWN');
|
||||
|
||||
$projectName = $project->getId() === 'console'
|
||||
? $platform['platformName']
|
||||
: $project->getAttribute('name');
|
||||
|
||||
$emailVariables = [
|
||||
'direction' => $locale->getText('settings.direction'),
|
||||
'date' => (new \DateTime())->format('F j'),
|
||||
'year' => (new \DateTime())->format('YYYY'),
|
||||
'time' => (new \DateTime())->format('H:i:s'),
|
||||
'user' => $event->user['name'] ?? '',
|
||||
'project' => $projectName,
|
||||
'device' => $clientName,
|
||||
'ipAddress' => $session->getAttribute('ip'),
|
||||
'country' => $locale->getText('countries.' . $session->getAttribute('countryCode'), $locale->getText('locale.country.unknown')),
|
||||
];
|
||||
|
||||
if ($isBranded) {
|
||||
$emailVariables += [
|
||||
'accentColor' => $platform['accentColor'],
|
||||
'logoUrl' => $platform['logoUrl'],
|
||||
'twitter' => $platform['twitterUrl'],
|
||||
'discord' => $platform['discordUrl'],
|
||||
'github' => $platform['githubUrl'],
|
||||
'terms' => $platform['termsUrl'],
|
||||
'privacy' => $platform['privacyUrl'],
|
||||
'platform' => $platform['platformName'],
|
||||
];
|
||||
}
|
||||
|
||||
$queueForMails = new Mail($publisher);
|
||||
|
||||
if ($smtp['enabled'] ?? false) {
|
||||
$queueForMails
|
||||
->setSmtpHost($smtp['host'] ?? '')
|
||||
->setSmtpPort($smtp['port'] ?? '')
|
||||
->setSmtpUsername($smtp['username'] ?? '')
|
||||
->setSmtpPassword($smtp['password'] ?? '')
|
||||
->setSmtpSecure($smtp['secure'] ?? '')
|
||||
->setSmtpReplyTo($customTemplate['replyTo'] ?? $smtp['replyTo'] ?? '')
|
||||
->setSmtpSenderEmail($customTemplate['senderEmail'] ?? $smtp['senderEmail'] ?? System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM))
|
||||
->setSmtpSenderName($customTemplate['senderName'] ?? $smtp['senderName'] ?? System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
|
||||
}
|
||||
|
||||
$queueForMails
|
||||
->setProject($project)
|
||||
->setSubject($subject)
|
||||
->setPreview($preview)
|
||||
->setBody($body)
|
||||
->setBodyTemplate(__DIR__ . '/../../../../app/config/locale/templates/' . $smtpBaseTemplate . '.tpl')
|
||||
->appendVariables($emailVariables)
|
||||
->setRecipient($event->user['email']);
|
||||
|
||||
if ($isBranded) {
|
||||
$queueForMails->setSenderName($platform['emailSenderName']);
|
||||
}
|
||||
|
||||
$queueForMails->trigger();
|
||||
}
|
||||
}
|
||||
@@ -285,7 +285,7 @@ class Event
|
||||
*
|
||||
* @param string $key
|
||||
* @param Document $context
|
||||
* @return self
|
||||
* @return static
|
||||
*/
|
||||
public function setContext(string $key, Document $context): self
|
||||
{
|
||||
@@ -309,7 +309,7 @@ class Event
|
||||
/**
|
||||
* Set class used for this event.
|
||||
* @param string $class
|
||||
* @return self
|
||||
* @return static
|
||||
*/
|
||||
public function setClass(string $class): self
|
||||
{
|
||||
@@ -648,10 +648,8 @@ class Event
|
||||
*
|
||||
* @param Event $event
|
||||
*
|
||||
* @return self
|
||||
*
|
||||
*/
|
||||
public function from(Event $event): self
|
||||
public function from(Event $event): static
|
||||
{
|
||||
$this->project = $event->getProject();
|
||||
$this->user = $event->getUser();
|
||||
|
||||
@@ -53,4 +53,23 @@ class Execution extends Event
|
||||
'execution' => $this->execution,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim payload for the execution event.
|
||||
* Only the project ID is needed — the worker DI fetches the full project from the platform database.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function trimPayload(): array
|
||||
{
|
||||
$trimmed = [];
|
||||
|
||||
if ($this->project) {
|
||||
$trimmed['project'] = new Document([
|
||||
'$id' => $this->project->getId(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,6 @@ class Mail extends Event
|
||||
/**
|
||||
* Sets preview for the mail event.
|
||||
*
|
||||
* @param string $preview
|
||||
* @return self
|
||||
*/
|
||||
public function setPreview(string $preview): self
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Message;
|
||||
|
||||
use Utopia\Database\Document;
|
||||
|
||||
final class Execution extends Base
|
||||
{
|
||||
public function __construct(
|
||||
public readonly Document $project,
|
||||
public readonly Document $execution,
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'project' => $this->project->getArrayCopy(),
|
||||
'execution' => $this->execution->getArrayCopy(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
return new self(
|
||||
project: new Document($data['project'] ?? []),
|
||||
execution: new Document($data['execution'] ?? []),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Message;
|
||||
|
||||
use Utopia\Database\Document;
|
||||
|
||||
final class Migration extends Base
|
||||
{
|
||||
public function __construct(
|
||||
public readonly Document $project,
|
||||
public readonly Document $migration,
|
||||
public readonly array $platform = [],
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'project' => $this->project->getArrayCopy(),
|
||||
'migration' => $this->migration->getArrayCopy(),
|
||||
'platform' => $this->platform,
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
return new self(
|
||||
project: new Document($data['project'] ?? []),
|
||||
migration: new Document($data['migration'] ?? []),
|
||||
platform: $data['platform'] ?? [],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Message;
|
||||
|
||||
use Utopia\Database\Document;
|
||||
|
||||
final class StatsResources extends Base
|
||||
{
|
||||
public function __construct(
|
||||
public readonly Document $project,
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'project' => $this->project->getArrayCopy(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
return new self(
|
||||
project: new Document($data['project'] ?? []),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ namespace Appwrite\Event\Message;
|
||||
|
||||
use Utopia\Database\Document;
|
||||
|
||||
final class Usage extends Base
|
||||
class Usage extends Base
|
||||
{
|
||||
/**
|
||||
* @param Document $project
|
||||
@@ -40,7 +40,8 @@ final class Usage extends Base
|
||||
*/
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
return new self(
|
||||
/** @phpstan-ignore new.static (subclass constructors are backwards-compatible via optional params) */
|
||||
return new static(
|
||||
project: new Document($data['project'] ?? []),
|
||||
metrics: $data['metrics'] ?? [],
|
||||
reduce: array_map(fn (array $doc) => new Document($doc), $data['reduce'] ?? []),
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Publisher;
|
||||
|
||||
use Appwrite\Event\Message\Execution as ExecutionMessage;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Queue\Queue;
|
||||
|
||||
readonly class Execution extends Base
|
||||
{
|
||||
public function __construct(
|
||||
Publisher $publisher,
|
||||
protected Queue $queue
|
||||
) {
|
||||
parent::__construct($publisher);
|
||||
}
|
||||
|
||||
public function enqueue(ExecutionMessage $message): string|bool
|
||||
{
|
||||
return $this->publish($this->queue, $message);
|
||||
}
|
||||
|
||||
public function getSize(bool $failed = false): int
|
||||
{
|
||||
return $this->getQueueSize($this->queue, $failed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Publisher;
|
||||
|
||||
use Appwrite\Event\Message\Migration as MigrationMessage;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Queue\Queue;
|
||||
|
||||
readonly class Migration extends Base
|
||||
{
|
||||
public function __construct(
|
||||
Publisher $publisher,
|
||||
protected Queue $queue
|
||||
) {
|
||||
parent::__construct($publisher);
|
||||
}
|
||||
|
||||
public function enqueue(MigrationMessage $message): string|bool
|
||||
{
|
||||
return $this->publish($this->queue, $message);
|
||||
}
|
||||
|
||||
public function getSize(bool $failed = false): int
|
||||
{
|
||||
return $this->getQueueSize($this->queue, $failed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Publisher;
|
||||
|
||||
use Appwrite\Event\Message\StatsResources as StatsResourcesMessage;
|
||||
use Utopia\Console;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Queue\Queue;
|
||||
|
||||
readonly class StatsResources extends Base
|
||||
{
|
||||
public function __construct(
|
||||
Publisher $publisher,
|
||||
protected Queue $queue
|
||||
) {
|
||||
parent::__construct($publisher);
|
||||
}
|
||||
|
||||
public function enqueue(StatsResourcesMessage $message): string|bool
|
||||
{
|
||||
// Resource stats are best-effort; publishing failures should not interrupt the scheduler loop.
|
||||
try {
|
||||
return $this->publish($this->queue, $message);
|
||||
} catch (\Throwable $th) {
|
||||
Console::error('[StatsResources] Failed to publish stats resources message: ' . $th->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getSize(bool $failed = false): int
|
||||
{
|
||||
return $this->getQueueSize($this->queue, $failed);
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,26 @@ class Realtime extends Event
|
||||
return $this->subscribers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the event state for long-lived worker processes.
|
||||
*
|
||||
* `Event::reset()` clears params/sensitive/event/payload only. Realtime routing also
|
||||
* depends on `context`, `subscribers`, and `project`/`user` fields, so we clear them too
|
||||
* to prevent stale state from affecting subsequent triggers.
|
||||
*/
|
||||
public function reset(): self
|
||||
{
|
||||
parent::reset();
|
||||
|
||||
$this->subscribers = [];
|
||||
$this->context = [];
|
||||
$this->project = null;
|
||||
$this->user = null;
|
||||
$this->userId = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute Event.
|
||||
*
|
||||
|
||||
@@ -82,6 +82,9 @@ class Exception extends \Exception
|
||||
public const string USER_PASSWORD_RECENTLY_USED = 'password_recently_used';
|
||||
public const string USER_PASSWORD_PERSONAL_DATA = 'password_personal_data';
|
||||
public const string USER_EMAIL_ALREADY_EXISTS = 'user_email_already_exists';
|
||||
public const string USER_EMAIL_DISPOSABLE = 'user_email_disposable';
|
||||
public const string USER_EMAIL_FREE = 'user_email_free';
|
||||
public const string USER_EMAIL_NOT_CANONICAL = 'user_email_not_canonical';
|
||||
public const string USER_PASSWORD_MISMATCH = 'user_password_mismatch';
|
||||
public const string USER_SESSION_NOT_FOUND = 'user_session_not_found';
|
||||
public const string USER_IDENTITY_NOT_FOUND = 'user_identity_not_found';
|
||||
@@ -330,6 +333,8 @@ class Exception extends \Exception
|
||||
|
||||
/** Platform */
|
||||
public const string PLATFORM_NOT_FOUND = 'platform_not_found';
|
||||
public const string PLATFORM_METHOD_UNSUPPORTED = 'platform_method_unsupported';
|
||||
public const string PLATFORM_ALREADY_EXISTS = 'platform_already_exists';
|
||||
|
||||
/** GraphqQL */
|
||||
public const string GRAPHQL_NO_QUERY = 'graphql_no_query';
|
||||
|
||||
@@ -81,8 +81,8 @@ abstract class Adapter implements PromiseAdapter
|
||||
/**
|
||||
* Create a new promise that resolves when all passed in promises resolve.
|
||||
*
|
||||
* @param array $promisesOrValues
|
||||
* @param iterable $promisesOrValues
|
||||
* @return GQLPromise
|
||||
*/
|
||||
abstract public function all(array $promisesOrValues): GQLPromise;
|
||||
abstract public function all(iterable $promisesOrValues): GQLPromise;
|
||||
}
|
||||
|
||||
@@ -35,8 +35,12 @@ class Swoole extends Adapter
|
||||
return new GQLPromise($promise, $this);
|
||||
}
|
||||
|
||||
public function all(array $promisesOrValues): GQLPromise
|
||||
public function all(iterable $promisesOrValues): GQLPromise
|
||||
{
|
||||
if ($promisesOrValues instanceof \Traversable) {
|
||||
$promisesOrValues = \iterator_to_array($promisesOrValues);
|
||||
}
|
||||
|
||||
return new GQLPromise(SwoolePromise::all($promisesOrValues), $this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,14 +25,10 @@ class Resolvers
|
||||
?Route $route,
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $route, $args, $context, $info) {
|
||||
/** @var Http $utopia */
|
||||
/** @var Response $response */
|
||||
/** @var Request $request */
|
||||
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
function (callable $resolve, callable $reject) use ($utopia, $route, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
|
||||
$path = $route->getPath();
|
||||
foreach ($args as $key => $value) {
|
||||
@@ -96,10 +92,10 @@ class Resolvers
|
||||
callable $url,
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
|
||||
$request->setMethod('GET');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
@@ -127,10 +123,10 @@ class Resolvers
|
||||
callable $params,
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
|
||||
$request->setMethod('GET');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
@@ -163,10 +159,10 @@ class Resolvers
|
||||
callable $params,
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
|
||||
$request->setMethod('POST');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
@@ -195,10 +191,10 @@ class Resolvers
|
||||
callable $params,
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
|
||||
$request->setMethod('PATCH');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
@@ -225,10 +221,10 @@ class Resolvers
|
||||
callable $url,
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
|
||||
$request->setMethod('DELETE');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
@@ -266,6 +262,9 @@ class Resolvers
|
||||
$request = clone $request;
|
||||
$utopia->setResource('request', static fn () => $request);
|
||||
$response->setContentType(Response::CONTENT_TYPE_NULL);
|
||||
$response->clearSent();
|
||||
|
||||
$original = $utopia->getRoute();
|
||||
|
||||
try {
|
||||
$route = $utopia->match($request, fresh: true);
|
||||
@@ -277,6 +276,10 @@ class Resolvers
|
||||
}
|
||||
$reject($e);
|
||||
return;
|
||||
} finally {
|
||||
if ($original !== null) {
|
||||
$utopia->setRoute($original);
|
||||
}
|
||||
}
|
||||
|
||||
$payload = $response->getPayload();
|
||||
|
||||
@@ -32,10 +32,6 @@ class Schema
|
||||
array $urls,
|
||||
array $params,
|
||||
): GQLSchema {
|
||||
Http::setResource('utopia:graphql', static function () use ($utopia) {
|
||||
return $utopia;
|
||||
});
|
||||
|
||||
if (!empty(self::$schema)) {
|
||||
return self::$schema;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
namespace Appwrite\GraphQL\Types;
|
||||
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\StringValueNode;
|
||||
|
||||
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
|
||||
class Assoc extends Json
|
||||
{
|
||||
public $name = 'Assoc';
|
||||
public $description = 'The `Assoc` scalar type represents associative array values.';
|
||||
public string $name = 'Assoc';
|
||||
public ?string $description = 'The `Assoc` scalar type represents associative array values.';
|
||||
|
||||
public function serialize($value)
|
||||
{
|
||||
@@ -30,6 +31,10 @@ class Assoc extends Json
|
||||
|
||||
public function parseLiteral(Node $valueNode, ?array $variables = null)
|
||||
{
|
||||
return \json_decode($valueNode->value, true);
|
||||
if ($valueNode instanceof StringValueNode) {
|
||||
return \json_decode($valueNode->value, true);
|
||||
}
|
||||
|
||||
return parent::parseLiteral($valueNode, $variables);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ use GraphQL\Type\Definition\ScalarType;
|
||||
|
||||
class InputFile extends ScalarType
|
||||
{
|
||||
public $name = 'InputFile';
|
||||
public $description = 'The `InputFile` special type represents a file to be uploaded in the same HTTP request as specified by
|
||||
public string $name = 'InputFile';
|
||||
public ?string $description = 'The `InputFile` special type represents a file to be uploaded in the same HTTP request as specified by
|
||||
[graphql-multipart-request-spec](https://github.com/jaydenseric/graphql-multipart-request-spec).';
|
||||
|
||||
public function serialize($value)
|
||||
|
||||
@@ -14,8 +14,8 @@ use GraphQL\Type\Definition\ScalarType;
|
||||
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
|
||||
class Json extends ScalarType
|
||||
{
|
||||
public $name = 'Json';
|
||||
public $description = 'The `JSON` scalar type represents JSON values as specified by
|
||||
public string $name = 'Json';
|
||||
public ?string $description = 'The `JSON` scalar type represents JSON values as specified by
|
||||
[ECMA-404](https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).';
|
||||
|
||||
public function serialize($value)
|
||||
|
||||
@@ -20,6 +20,7 @@ class Realtime extends MessagingAdapter
|
||||
* [CONNECTION_ID] ->
|
||||
* 'projectId' -> [PROJECT_ID]
|
||||
* 'roles' -> [ROLE_x, ROLE_Y]
|
||||
* 'userId' -> [USER_ID]
|
||||
* 'channels' -> [CHANNEL_NAME_X, CHANNEL_NAME_Y, CHANNEL_NAME_Z]
|
||||
*/
|
||||
public array $connections = [];
|
||||
@@ -67,25 +68,35 @@ class Realtime extends MessagingAdapter
|
||||
* @param array $queryGroup Array of Query objects for this subscription (AND logic within subscription)
|
||||
* @return void
|
||||
*/
|
||||
public function subscribe(string $projectId, mixed $identifier, string $subscriptionId, array $roles, array $channels, array $queryGroup = []): void
|
||||
{
|
||||
public function subscribe(
|
||||
string $projectId,
|
||||
mixed $identifier,
|
||||
string $subscriptionId,
|
||||
array $roles,
|
||||
array $channels,
|
||||
array $queryGroup = [],
|
||||
?string $userId = null
|
||||
): void {
|
||||
if (!isset($this->subscriptions[$projectId])) { // Init Project
|
||||
$this->subscriptions[$projectId] = [];
|
||||
}
|
||||
|
||||
$strings = [];
|
||||
if (empty($queryGroup)) {
|
||||
$strings[] = Query::select(['*'])->toString();
|
||||
} else {
|
||||
foreach ($queryGroup as $query) {
|
||||
$strings[] = $query->toString();
|
||||
}
|
||||
}
|
||||
$data = [];
|
||||
|
||||
$data = [
|
||||
'strings' => $strings,
|
||||
'compiled' => RuntimeQuery::compile($queryGroup),
|
||||
];
|
||||
if (!empty($channels)) {
|
||||
if (empty($queryGroup)) {
|
||||
$strings[] = Query::select(['*'])->toString();
|
||||
} else {
|
||||
foreach ($queryGroup as $query) {
|
||||
$strings[] = $query->toString();
|
||||
}
|
||||
}
|
||||
$data = [
|
||||
'strings' => $strings,
|
||||
'compiled' => RuntimeQuery::compile($queryGroup),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($roles as $role) {
|
||||
if (!isset($this->subscriptions[$projectId][$role])) {
|
||||
@@ -103,10 +114,12 @@ class Realtime extends MessagingAdapter
|
||||
}
|
||||
}
|
||||
|
||||
// Update connection info
|
||||
// Keep userId from onOpen/authentication when provided.
|
||||
// Fallback to existing stored value for subsequent subscribe upserts.
|
||||
$this->connections[$identifier] = [
|
||||
'projectId' => $projectId,
|
||||
'roles' => $roles,
|
||||
'userId' => $userId ?? ($this->connections[$identifier]['userId'] ?? ''),
|
||||
'channels' => $channels
|
||||
];
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ abstract class Migration
|
||||
'1.8.0' => 'V23',
|
||||
'1.8.1' => 'V23',
|
||||
'1.9.0' => 'V24',
|
||||
'1.9.1' => 'V24',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -48,7 +48,7 @@ final class Cors
|
||||
/**
|
||||
* Build CORS headers for a given request origin.
|
||||
*
|
||||
* @return array<string,string>
|
||||
* @return array<string, int|string>
|
||||
*/
|
||||
public function headers(string $origin): array
|
||||
{
|
||||
|
||||
@@ -6,20 +6,10 @@ class Platform
|
||||
{
|
||||
public const TYPE_UNKNOWN = 'unknown';
|
||||
public const TYPE_WEB = 'web';
|
||||
public const TYPE_FLUTTER_IOS = 'flutter-ios';
|
||||
public const TYPE_FLUTTER_ANDROID = 'flutter-android';
|
||||
public const TYPE_FLUTTER_MACOS = 'flutter-macos';
|
||||
public const TYPE_FLUTTER_WINDOWS = 'flutter-windows';
|
||||
public const TYPE_FLUTTER_LINUX = 'flutter-linux';
|
||||
public const TYPE_FLUTTER_WEB = 'flutter-web';
|
||||
public const TYPE_APPLE_IOS = 'apple-ios';
|
||||
public const TYPE_APPLE_MACOS = 'apple-macos';
|
||||
public const TYPE_APPLE_WATCHOS = 'apple-watchos';
|
||||
public const TYPE_APPLE_TVOS = 'apple-tvos';
|
||||
public const TYPE_APPLE = 'apple';
|
||||
public const TYPE_ANDROID = 'android';
|
||||
public const TYPE_UNITY = 'unity';
|
||||
public const TYPE_REACT_NATIVE_IOS = 'react-native-ios';
|
||||
public const TYPE_REACT_NATIVE_ANDROID = 'react-native-android';
|
||||
public const TYPE_WINDOWS = 'windows';
|
||||
public const TYPE_LINUX = 'linux';
|
||||
public const TYPE_SCHEME = 'scheme';
|
||||
|
||||
public const SCHEME_HTTP = 'http';
|
||||
@@ -57,6 +47,36 @@ class Platform
|
||||
self::SCHEME_TAURI => 'Web (Tauri)',
|
||||
];
|
||||
|
||||
/**
|
||||
* Map deprecated platform types to their new consolidated types.
|
||||
*
|
||||
* The 1.9.x refactor consolidated ~15 platform types into 5 new ones.
|
||||
* Existing platforms in the database may still have old type values.
|
||||
*
|
||||
* @param string $type
|
||||
* @return string The mapped type, or the original if not deprecated.
|
||||
*/
|
||||
public static function mapDeprecatedType(string $type): string
|
||||
{
|
||||
$mapping = [
|
||||
'flutter-web' => self::TYPE_WEB,
|
||||
'unity' => self::TYPE_WEB,
|
||||
'flutter-ios' => self::TYPE_APPLE,
|
||||
'flutter-macos' => self::TYPE_APPLE,
|
||||
'apple-ios' => self::TYPE_APPLE,
|
||||
'apple-macos' => self::TYPE_APPLE,
|
||||
'apple-watchos' => self::TYPE_APPLE,
|
||||
'apple-tvos' => self::TYPE_APPLE,
|
||||
'react-native-ios' => self::TYPE_APPLE,
|
||||
'flutter-android' => self::TYPE_ANDROID,
|
||||
'react-native-android' => self::TYPE_ANDROID,
|
||||
'flutter-windows' => self::TYPE_WINDOWS,
|
||||
'flutter-linux' => self::TYPE_LINUX,
|
||||
];
|
||||
|
||||
return $mapping[$type] ?? $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-friendly platform name from a scheme.
|
||||
*
|
||||
@@ -77,25 +97,28 @@ class Platform
|
||||
$key = strtolower($platform['key'] ?? '');
|
||||
|
||||
switch ($type) {
|
||||
case 'flutter-web':
|
||||
case 'unity':
|
||||
case self::TYPE_WEB:
|
||||
case self::TYPE_FLUTTER_WEB:
|
||||
if (!empty($hostname)) {
|
||||
$hostnames[] = $hostname;
|
||||
}
|
||||
break;
|
||||
case self::TYPE_FLUTTER_IOS:
|
||||
case self::TYPE_FLUTTER_ANDROID:
|
||||
case self::TYPE_FLUTTER_MACOS:
|
||||
case self::TYPE_FLUTTER_WINDOWS:
|
||||
case self::TYPE_FLUTTER_LINUX:
|
||||
case 'flutter-android':
|
||||
case 'react-native-android':
|
||||
case self::TYPE_ANDROID:
|
||||
case self::TYPE_APPLE_IOS:
|
||||
case self::TYPE_APPLE_MACOS:
|
||||
case self::TYPE_APPLE_WATCHOS:
|
||||
case self::TYPE_APPLE_TVOS:
|
||||
case self::TYPE_REACT_NATIVE_IOS:
|
||||
case self::TYPE_REACT_NATIVE_ANDROID:
|
||||
case self::TYPE_UNITY:
|
||||
case 'flutter-windows':
|
||||
case self::TYPE_WINDOWS:
|
||||
case 'flutter-linux':
|
||||
case self::TYPE_LINUX:
|
||||
case 'flutter-ios':
|
||||
case 'flutter-macos':
|
||||
case 'apple-ios':
|
||||
case 'apple-macos':
|
||||
case 'apple-watchos':
|
||||
case 'apple-tvos':
|
||||
case 'react-native-ios':
|
||||
case self::TYPE_APPLE:
|
||||
if (!empty($key)) {
|
||||
$hostnames[] = $key;
|
||||
}
|
||||
@@ -120,38 +143,38 @@ class Platform
|
||||
$schemes[] = $scheme;
|
||||
}
|
||||
break;
|
||||
case 'flutter-web':
|
||||
case 'unity':
|
||||
case self::TYPE_WEB:
|
||||
case self::TYPE_FLUTTER_WEB:
|
||||
$schemes[] = self::SCHEME_HTTP;
|
||||
$schemes[] = self::SCHEME_HTTPS;
|
||||
break;
|
||||
case self::TYPE_FLUTTER_IOS:
|
||||
case self::TYPE_APPLE_IOS:
|
||||
case self::TYPE_REACT_NATIVE_IOS:
|
||||
$schemes[] = self::SCHEME_IOS;
|
||||
break;
|
||||
case self::TYPE_FLUTTER_ANDROID:
|
||||
case 'flutter-android':
|
||||
case 'react-native-android':
|
||||
case self::TYPE_ANDROID:
|
||||
case self::TYPE_REACT_NATIVE_ANDROID:
|
||||
$schemes[] = self::SCHEME_ANDROID;
|
||||
break;
|
||||
case self::TYPE_FLUTTER_MACOS:
|
||||
case self::TYPE_APPLE_MACOS:
|
||||
case 'flutter-ios':
|
||||
case 'flutter-macos':
|
||||
case 'apple-ios':
|
||||
case 'apple-macos':
|
||||
case 'apple-watchos':
|
||||
case 'apple-tvos':
|
||||
case 'react-native-ios':
|
||||
case self::TYPE_APPLE:
|
||||
$schemes[] = self::SCHEME_WATCHOS;
|
||||
$schemes[] = self::SCHEME_MACOS;
|
||||
$schemes[] = self::SCHEME_TVOS;
|
||||
$schemes[] = self::SCHEME_IOS;
|
||||
break;
|
||||
case self::TYPE_FLUTTER_WINDOWS:
|
||||
case self::TYPE_UNITY:
|
||||
case 'flutter-windows':
|
||||
case self::TYPE_WINDOWS:
|
||||
$schemes[] = self::SCHEME_WINDOWS;
|
||||
break;
|
||||
case self::TYPE_FLUTTER_LINUX:
|
||||
case 'flutter-linux':
|
||||
case self::TYPE_LINUX:
|
||||
$schemes[] = self::SCHEME_LINUX;
|
||||
break;
|
||||
case self::TYPE_APPLE_WATCHOS:
|
||||
$schemes[] = self::SCHEME_WATCHOS;
|
||||
break;
|
||||
case self::TYPE_APPLE_TVOS:
|
||||
$schemes[] = self::SCHEME_TVOS;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class OpenSSL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function encrypt($data, $method, $key, $options = 0, $iv = '', &$tag = null, $aad = '', $tag_length = 16)
|
||||
public static function encrypt($data, $method, $key, $options = 0, $iv = '', ?string &$tag = null, $aad = '', $tag_length = 16)
|
||||
{
|
||||
return \openssl_encrypt($data, $method, $key, $options, $iv, $tag, $aad, $tag_length);
|
||||
}
|
||||
|
||||
@@ -145,9 +145,20 @@ class Server
|
||||
$paths = $this->paths;
|
||||
$state = $this->state;
|
||||
|
||||
Http::setResource('installerState', fn () => $state);
|
||||
Http::setResource('installerConfig', fn () => $config);
|
||||
Http::setResource('installerPaths', fn () => $paths);
|
||||
$adapter = new class ($host, $port, ['worker_num' => 1]) extends SwooleAdapter {
|
||||
public function getNativeServer(): SwooleServer
|
||||
{
|
||||
return $this->server;
|
||||
}
|
||||
};
|
||||
|
||||
$nativeServer = $adapter->getNativeServer();
|
||||
|
||||
$container = $adapter->getContainer();
|
||||
$container->set('installerState', fn () => $state);
|
||||
$container->set('installerConfig', fn () => $config);
|
||||
$container->set('installerPaths', fn () => $paths);
|
||||
$container->set('swooleServer', fn () => $nativeServer);
|
||||
|
||||
// Register routes via Utopia Platform
|
||||
$platform = new Installer();
|
||||
@@ -160,17 +171,6 @@ class Server
|
||||
->inject('response')
|
||||
->action($errorHandler->action(...));
|
||||
|
||||
$adapter = new class ($host, $port, ['worker_num' => 1]) extends SwooleAdapter {
|
||||
public function getNativeServer(): SwooleServer
|
||||
{
|
||||
return $this->server;
|
||||
}
|
||||
};
|
||||
|
||||
$nativeServer = $adapter->getNativeServer();
|
||||
|
||||
Http::setResource('swooleServer', fn () => $nativeServer);
|
||||
|
||||
$nativeServer->on('start', function () use ($nativeServer, $port, $readyFile) {
|
||||
\Swoole\Process::signal(SIGTERM, fn () => $nativeServer->shutdown());
|
||||
\Swoole\Process::signal(SIGINT, fn () => $nativeServer->shutdown());
|
||||
@@ -180,7 +180,7 @@ class Server
|
||||
}
|
||||
});
|
||||
|
||||
$adapter->onRequest(function (Request $request, Response $response) use ($files) {
|
||||
$adapter->onRequest(function (Request $request, Response $response) use ($adapter, $files) {
|
||||
// Serve static files from memory
|
||||
$uri = $request->getURI();
|
||||
if ($files->isFileLoaded($uri)) {
|
||||
@@ -190,7 +190,7 @@ class Server
|
||||
return;
|
||||
}
|
||||
|
||||
$app = new Http('UTC');
|
||||
$app = new Http($adapter, 'UTC');
|
||||
$app->run($request, $response);
|
||||
});
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ class Action extends PlatformAction
|
||||
|
||||
$image = new Image(\file_get_contents($path));
|
||||
$image->crop((int) $width, (int) $height);
|
||||
$output = (empty($output)) ? $type : $output;
|
||||
$data = $image->output($output, $quality);
|
||||
$response
|
||||
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
|
||||
|
||||
@@ -204,7 +204,6 @@ class Get extends Action
|
||||
|
||||
$image = new Image($data);
|
||||
$image->crop((int) $width, (int) $height);
|
||||
$output = (empty($output)) ? $type : $output;
|
||||
$data = $image->output($output, $quality);
|
||||
|
||||
$response
|
||||
|
||||
@@ -95,7 +95,6 @@ class Get extends Action
|
||||
}
|
||||
|
||||
$image->crop((int) $width, (int) $height);
|
||||
$output = (empty($output)) ? $type : $output;
|
||||
$data = $image->output($output, $quality);
|
||||
|
||||
$response
|
||||
|
||||
@@ -90,7 +90,7 @@ class Get extends Action
|
||||
}
|
||||
}
|
||||
|
||||
$rand = \substr($code, -1);
|
||||
$rand = (int) \substr((string) $code, -1);
|
||||
|
||||
$rand = ($rand > \count($themes) - 1) ? $rand % \count($themes) : $rand;
|
||||
|
||||
|
||||
@@ -7,9 +7,13 @@ use Appwrite\Platform\Action as AppwriteAction;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Operator;
|
||||
use Utopia\Database\Query;
|
||||
|
||||
class Action extends AppwriteAction
|
||||
{
|
||||
public const LIST_CACHE_FIELD_DOCUMENTS = 'documents';
|
||||
public const LIST_CACHE_FIELD_TOTAL = 'total';
|
||||
|
||||
private string $context = DATABASE_TYPE_LEGACY;
|
||||
|
||||
public function getDatabaseType(): string
|
||||
@@ -17,7 +21,7 @@ class Action extends AppwriteAction
|
||||
return $this->context;
|
||||
}
|
||||
|
||||
public function setHttpPath(string $path): AppwriteAction
|
||||
public function setHttpPath(string $path): self
|
||||
{
|
||||
if (\str_contains($path, '/tablesdb')) {
|
||||
$this->context = DATABASE_TYPE_TABLESDB;
|
||||
@@ -28,7 +32,8 @@ class Action extends AppwriteAction
|
||||
if (\str_contains($path, '/vectorsdb')) {
|
||||
$this->context = DATABASE_TYPE_VECTORSDB;
|
||||
}
|
||||
return parent::setHttpPath($path);
|
||||
parent::setHttpPath($path);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,4 +105,67 @@ class Action extends AppwriteAction
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stable Redis key for a collection's cached list responses.
|
||||
*
|
||||
* All variations (schema × roles × queries) for a single collection live as
|
||||
* fields inside this one Redis hash, so purging every cached entry for a
|
||||
* collection is a single O(1) DEL regardless of how many variations have
|
||||
* been cached.
|
||||
*/
|
||||
protected function getListCacheKey(Database $dbForProject, string $collectionId): string
|
||||
{
|
||||
return \sprintf(
|
||||
'%s-cache:%s:%s:%s:collection:%s',
|
||||
$dbForProject->getCacheName(),
|
||||
$dbForProject->getAdapter()->getHostname(),
|
||||
$dbForProject->getNamespace(),
|
||||
$dbForProject->getTenant(),
|
||||
$collectionId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash field for a single variation of a cached list response.
|
||||
*
|
||||
* Scoped by the collection schema (attributes + indexes), the caller's
|
||||
* authorization roles, the exact query set, and the field type — so users
|
||||
* with different permissions never share entries.
|
||||
*
|
||||
* @param Document $collection Collection document (for schema hash)
|
||||
* @param array<mixed> $roles Caller authorization roles
|
||||
* @param array<Query|string> $queries Queries for this list call
|
||||
* @param string $type LIST_CACHE_FIELD_DOCUMENTS or LIST_CACHE_FIELD_TOTAL
|
||||
*/
|
||||
protected function getListCacheField(Document $collection, array $roles, array $queries, string $type): string
|
||||
{
|
||||
$schemaHash = \md5(
|
||||
\json_encode($collection->getAttribute('attributes', []))
|
||||
. \json_encode($collection->getAttribute('indexes', []))
|
||||
);
|
||||
|
||||
$serialized = \array_map(
|
||||
static fn ($query) => $query instanceof Query ? $query->toArray() : $query,
|
||||
$queries,
|
||||
);
|
||||
|
||||
return \sprintf(
|
||||
'%s:%s:%s:%s',
|
||||
$schemaHash,
|
||||
\md5(\json_encode($roles)),
|
||||
\md5(\json_encode($serialized)),
|
||||
$type,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge every cached list response for a collection.
|
||||
*
|
||||
* One DEL on the collection's Redis hash, clearing all variations at once.
|
||||
*/
|
||||
protected function purgeListCache(Database $dbForProject, string $collectionId): bool
|
||||
{
|
||||
return $dbForProject->getCache()->purge($this->getListCacheKey($dbForProject, $collectionId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,34 +3,29 @@
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Action as DatabasesAction;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Platform\Action as UtopiaAction;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
abstract class Action extends UtopiaAction
|
||||
abstract class Action extends DatabasesAction
|
||||
{
|
||||
/**
|
||||
* The current API context (either 'table' or 'collection').
|
||||
*/
|
||||
private ?string $context = COLLECTIONS;
|
||||
|
||||
private ?string $databaseType = LEGACY;
|
||||
|
||||
/**
|
||||
* Get the response model used in the SDK and HTTP responses.
|
||||
*/
|
||||
abstract protected function getResponseModel(): string;
|
||||
|
||||
public function setHttpPath(string $path): UtopiaAction
|
||||
public function setHttpPath(string $path): self
|
||||
{
|
||||
if (\str_contains($path, '/tablesdb')) {
|
||||
$this->context = TABLES;
|
||||
$this->databaseType = TABLESDB;
|
||||
} elseif (\str_contains($path, '/vectorsdb')) {
|
||||
$this->databaseType = VECTORSDB;
|
||||
}
|
||||
return parent::setHttpPath($path);
|
||||
parent::setHttpPath($path);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,14 +36,6 @@ abstract class Action extends UtopiaAction
|
||||
return $this->context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current API database type.
|
||||
*/
|
||||
protected function getDatabaseType(): string
|
||||
{
|
||||
return $this->databaseType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key used in event parameters (e.g., 'collectionId' or 'tableId').
|
||||
*/
|
||||
|
||||
+3
-4
@@ -24,7 +24,7 @@ abstract class Action extends DatabasesAction
|
||||
*/
|
||||
abstract protected function getResponseModel(): string;
|
||||
|
||||
public function setHttpPath(string $path): DatabasesAction
|
||||
public function setHttpPath(string $path): self
|
||||
{
|
||||
if (str_contains($path, '/tablesdb/')) {
|
||||
$this->context = ROWS;
|
||||
@@ -47,7 +47,8 @@ abstract class Action extends DatabasesAction
|
||||
],
|
||||
];
|
||||
|
||||
return parent::setHttpPath($path);
|
||||
parent::setHttpPath($path);
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function getDatabasesOperationReadMetric(): string
|
||||
@@ -406,8 +407,6 @@ abstract class Action extends DatabasesAction
|
||||
|
||||
if (\is_array($related)) {
|
||||
$document->setAttribute($relationship->getAttribute('key'), \array_values($relations));
|
||||
} elseif (empty($relations)) {
|
||||
$document->setAttribute($relationship->getAttribute('key'), null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -207,6 +207,8 @@ class Decrement extends Action
|
||||
->addMetric($this->getDatabasesOperationWriteMetric(), 1)
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), 1);
|
||||
|
||||
$response->dynamic($document, $this->getResponseModel());
|
||||
|
||||
$queueForEvents
|
||||
->setParam('databaseId', $databaseId)
|
||||
->setParam('collectionId', $collectionId)
|
||||
@@ -216,7 +218,5 @@ class Decrement extends Action
|
||||
->setContext('database', $database)
|
||||
->setContext($this->getCollectionsEventsContext(), $collection)
|
||||
->setPayload($response->getPayload(), sensitive: $relationships);
|
||||
|
||||
$response->dynamic($document, $this->getResponseModel());
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -207,6 +207,8 @@ class Increment extends Action
|
||||
->addMetric($this->getDatabasesOperationWriteMetric(), 1)
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), 1);
|
||||
|
||||
$response->dynamic($document, $this->getResponseModel());
|
||||
|
||||
$queueForEvents
|
||||
->setParam('databaseId', $databaseId)
|
||||
->setParam('collectionId', $collectionId)
|
||||
@@ -216,7 +218,5 @@ class Increment extends Action
|
||||
->setContext('database', $database)
|
||||
->setContext($this->getCollectionsEventsContext(), $collection)
|
||||
->setPayload($response->getPayload(), sensitive: $relationships);
|
||||
|
||||
$response->dynamic($document, $this->getResponseModel());
|
||||
}
|
||||
}
|
||||
|
||||
+25
-8
@@ -50,6 +50,11 @@ class Create extends Action
|
||||
return UtopiaResponse::MODEL_DOCUMENT_LIST;
|
||||
}
|
||||
|
||||
protected function getSupportForEmptyDocument()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
@@ -139,30 +144,42 @@ class Create extends Action
|
||||
->inject('eventProcessor')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
|
||||
{
|
||||
$data = \is_string($data)
|
||||
? \json_decode($data, true)
|
||||
: $data;
|
||||
|
||||
$supportsEmptyDocument = $this->getSupportForEmptyDocument();
|
||||
$hasData = !empty($data);
|
||||
$hasDocuments = !empty($documents);
|
||||
|
||||
/**
|
||||
* Determine which internal path to call, single or bulk
|
||||
*/
|
||||
if (empty($data) && empty($documents)) {
|
||||
if (!$supportsEmptyDocument && !$hasData && !$hasDocuments) {
|
||||
// No single or bulk documents provided
|
||||
throw new Exception($this->getMissingDataException());
|
||||
}
|
||||
if (!empty($data) && !empty($documents)) {
|
||||
|
||||
// When empty documents are supported, an empty payload should still be treated as single create.
|
||||
if ($supportsEmptyDocument && !$hasData && !$hasDocuments) {
|
||||
$data = [];
|
||||
$hasData = true;
|
||||
}
|
||||
|
||||
if ($hasData && $hasDocuments) {
|
||||
// Both single and bulk documents provided
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'You can only send one of the following parameters: data, ' . $this->getSDKGroup());
|
||||
}
|
||||
if (!empty($data) && empty($documentId)) {
|
||||
if ($hasData && empty($documentId)) {
|
||||
// Single document provided without document ID
|
||||
$document = $this->isCollectionsAPI() ? 'Document' : 'Row';
|
||||
$message = "$document ID is required when creating a single " . strtolower($document) . '.';
|
||||
throw new Exception($this->getMissingDataException(), $message);
|
||||
}
|
||||
if (!empty($documents) && !empty($documentId)) {
|
||||
if ($hasDocuments && !empty($documentId)) {
|
||||
// Bulk documents provided with document ID
|
||||
$documentId = $this->isCollectionsAPI() ? 'documentId' : 'rowId';
|
||||
throw new Exception(
|
||||
@@ -170,13 +187,13 @@ class Create extends Action
|
||||
"Param \"$documentId\" is not allowed when creating multiple " . $this->getSDKGroup() . ', set "$id" on each instead.'
|
||||
);
|
||||
}
|
||||
if (!empty($documents) && !empty($permissions)) {
|
||||
if ($hasDocuments && !empty($permissions)) {
|
||||
// Bulk documents provided with permissions
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "permissions" is disallowed when creating multiple ' . $this->getSDKGroup() . ', set "$permissions" on each instead');
|
||||
}
|
||||
|
||||
$isBulk = true;
|
||||
if (!empty($data)) {
|
||||
$isBulk = $hasDocuments;
|
||||
if ($hasData) {
|
||||
// Single document provided, convert to single item array
|
||||
// But remember that it was single to respond with a single document
|
||||
$isBulk = false;
|
||||
@@ -209,7 +226,7 @@ class Create extends Action
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk create is not supported for ' . $this->getSDKNamespace() . ' with relationship ' . $this->getStructureContext());
|
||||
}
|
||||
|
||||
$setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk, $dbForProject, $authorization) {
|
||||
$setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk, $authorization) {
|
||||
$allowedPermissions = [
|
||||
Database::PERMISSION_READ,
|
||||
Database::PERMISSION_UPDATE,
|
||||
|
||||
+1
@@ -131,6 +131,7 @@ class XList extends Action
|
||||
'userEmail' => $log['data']['userEmail'] ?? null,
|
||||
'userName' => $log['data']['userName'] ?? null,
|
||||
'mode' => $log['data']['mode'] ?? null,
|
||||
'userType' => $log['data']['userType'] ?? null,
|
||||
'ip' => $log['ip'],
|
||||
'time' => $log['time'],
|
||||
'osCode' => $os['osCode'],
|
||||
|
||||
-1
@@ -122,7 +122,6 @@ class Update extends Action
|
||||
|
||||
$dbForDatabases = $getDatabasesDB($database);
|
||||
// Read permission should not be required for update
|
||||
/** @var Document $document */
|
||||
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
|
||||
|
||||
if ($transactionId !== null) {
|
||||
|
||||
+58
-70
@@ -72,7 +72,7 @@ class XList extends Action
|
||||
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
|
||||
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID to read uncommitted changes within the transaction.', true, ['dbForProject'])
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->param('ttl', 0, new Range(min: 0, max: 86400), 'TTL (seconds) for cached responses when caching is enabled for select queries. Must be between 0 and 86400 (24 hours).', true)
|
||||
->param('ttl', 0, new Range(min: 0, max: 86400), 'TTL (seconds) for caching list responses. Responses are stored in an in-memory key-value cache, keyed per project, collection, schema version (attributes and indexes), caller authorization roles, and the exact query — so users with different permissions never share cached entries. Schema changes invalidate cached entries automatically; document writes do not, so choose a TTL you are comfortable serving as stale data. Set to 0 to disable caching. Must be between 0 and 86400 (24 hours).', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('user')
|
||||
@@ -127,84 +127,72 @@ class XList extends Action
|
||||
}
|
||||
|
||||
try {
|
||||
$selectQueries = Query::groupByType($queries)['selections'] ?? [];
|
||||
$hasSelects = ! empty(Query::groupByType($queries)['selections'] ?? []);
|
||||
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
|
||||
// When there are no select queries, relationship loading is skipped on the
|
||||
// underlying find() to avoid pulling related documents the caller did not ask for.
|
||||
$find = $hasSelects
|
||||
? fn () => $dbForDatabases->find($collectionTableId, $queries)
|
||||
: fn () => $dbForDatabases->skipRelationships(fn () => $dbForDatabases->find($collectionTableId, $queries));
|
||||
|
||||
// Use transaction-aware document retrieval if transactionId is provided
|
||||
if ($transactionId !== null) {
|
||||
$documents = $transactionState->listDocuments($database, $collectionTableId, $transactionId, $queries);
|
||||
$total = $includeTotal ? $transactionState->countDocuments($database, $collectionTableId, $transactionId, $queries) : 0;
|
||||
} elseif (! empty($selectQueries)) {
|
||||
} elseif ((int)$ttl > 0) {
|
||||
$cacheKey = $this->getListCacheKey($dbForProject, $collectionId);
|
||||
$roles = $dbForProject->getAuthorization()->getRoles();
|
||||
$documentsField = $this->getListCacheField($collection, $roles, $queries, self::LIST_CACHE_FIELD_DOCUMENTS);
|
||||
|
||||
if ((int)$ttl > 0) {
|
||||
$serializedQueries = [];
|
||||
foreach ($queries as $query) {
|
||||
$serializedQueries[] = $query instanceof Query ? $query->toArray() : $query;
|
||||
}
|
||||
|
||||
$hostname = $dbForProject->getAdapter()->getHostname();
|
||||
$roles = $dbForProject->getAuthorization()->getRoles();
|
||||
$schemaHash = \md5(\json_encode($collection->getAttribute('attributes', [])) . \json_encode($collection->getAttribute('indexes', [])));
|
||||
$cacheKeyBase = \sprintf(
|
||||
'%s-cache-%s:%s:%s:collection:%s:%s:user:%s:%s',
|
||||
$dbForProject->getCacheName(),
|
||||
$hostname ?? '',
|
||||
$dbForProject->getNamespace(),
|
||||
$dbForProject->getTenant(),
|
||||
$collectionId,
|
||||
$schemaHash,
|
||||
\md5(\json_encode($roles)),
|
||||
\md5(\json_encode($serializedQueries))
|
||||
);
|
||||
|
||||
$documentsCacheKey = $cacheKeyBase . ':documents';
|
||||
$totalCacheKey = $cacheKeyBase . ':total';
|
||||
|
||||
$documentsCacheHit = $totalDocumentsCacheHit = false;
|
||||
|
||||
$cachedDocuments = $dbForProject->getCache()->load($documentsCacheKey, $ttl);
|
||||
|
||||
if ($cachedDocuments !== null &&
|
||||
$cachedDocuments !== false &&
|
||||
\is_array($cachedDocuments)) {
|
||||
$documents = \array_map(function ($doc) {
|
||||
return new Document($doc);
|
||||
}, $cachedDocuments);
|
||||
$documentsCacheHit = true;
|
||||
} else {
|
||||
$documents = $dbForDatabases->find($collectionTableId, $queries);
|
||||
|
||||
// Convert Document objects to arrays for caching
|
||||
$documentsArray = \array_map(function ($doc) {
|
||||
return $doc->getArrayCopy();
|
||||
}, $documents);
|
||||
$dbForProject->getCache()->save($documentsCacheKey, $documentsArray);
|
||||
}
|
||||
|
||||
if ($includeTotal) {
|
||||
$cachedTotal = $dbForProject->getCache()->load($totalCacheKey, $ttl);
|
||||
if ($cachedTotal !== null && $cachedTotal !== false) {
|
||||
$total = $cachedTotal;
|
||||
$totalDocumentsCacheHit = true;
|
||||
} else {
|
||||
$total = $dbForProject->count($collectionTableId, $queries, APP_LIMIT_COUNT);
|
||||
$dbForProject->getCache()->save($totalCacheKey, $total);
|
||||
}
|
||||
} else {
|
||||
$total = 0;
|
||||
}
|
||||
|
||||
$response->addHeader('X-Appwrite-Cache', $documentsCacheHit ? 'hit' : 'miss');
|
||||
|
||||
} else {
|
||||
// has selects, allow relationship on documents
|
||||
$documents = $dbForDatabases->find($collectionTableId, $queries);
|
||||
$total = $includeTotal ? $dbForDatabases->count($collectionTableId, $queries, APP_LIMIT_COUNT) : 0;
|
||||
$documentsCacheHit = false;
|
||||
try {
|
||||
$cachedDocuments = $dbForProject->getCache()->load($cacheKey, $ttl, $documentsField);
|
||||
} catch (\Throwable) {
|
||||
$cachedDocuments = null;
|
||||
}
|
||||
|
||||
if ($cachedDocuments !== null &&
|
||||
$cachedDocuments !== false &&
|
||||
\is_array($cachedDocuments)) {
|
||||
$documents = \array_map(function ($doc) {
|
||||
return new Document($doc);
|
||||
}, $cachedDocuments);
|
||||
$documentsCacheHit = true;
|
||||
} else {
|
||||
$documents = $find();
|
||||
|
||||
$documentsArray = \array_map(function ($doc) {
|
||||
return $doc->getArrayCopy();
|
||||
}, $documents);
|
||||
try {
|
||||
$dbForProject->getCache()->save($cacheKey, $documentsArray, $documentsField);
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
if ($includeTotal) {
|
||||
$totalField = $this->getListCacheField($collection, $roles, $queries, self::LIST_CACHE_FIELD_TOTAL);
|
||||
try {
|
||||
$cachedTotal = $dbForProject->getCache()->load($cacheKey, $ttl, $totalField);
|
||||
} catch (\Throwable) {
|
||||
$cachedTotal = null;
|
||||
}
|
||||
if ($cachedTotal !== null && $cachedTotal !== false) {
|
||||
$total = $cachedTotal;
|
||||
} else {
|
||||
$total = $dbForDatabases->count($collectionTableId, $queries, APP_LIMIT_COUNT);
|
||||
try {
|
||||
$dbForProject->getCache()->save($cacheKey, $total, $totalField);
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$total = 0;
|
||||
}
|
||||
|
||||
$response->addHeader('X-Appwrite-Cache', $documentsCacheHit ? 'hit' : 'miss');
|
||||
} else {
|
||||
// has no selects, disable relationship loading on documents
|
||||
/* @type Document[] $documents */
|
||||
$documents = $dbForDatabases->skipRelationships(fn () => $dbForDatabases->find($collectionTableId, $queries));
|
||||
$documents = $find();
|
||||
$total = $includeTotal ? $dbForDatabases->count($collectionTableId, $queries, APP_LIMIT_COUNT) : 0;
|
||||
}
|
||||
} catch (OrderException $e) {
|
||||
|
||||
@@ -123,6 +123,7 @@ class XList extends Action
|
||||
'userEmail' => $log['data']['userEmail'] ?? null,
|
||||
'userName' => $log['data']['userName'] ?? null,
|
||||
'mode' => $log['data']['mode'] ?? null,
|
||||
'userType' => $log['data']['userType'] ?? null,
|
||||
'ip' => $log['ip'] ?? null,
|
||||
'time' => $log['time'] ?? null,
|
||||
'osCode' => $os['osCode'] ?? null,
|
||||
|
||||
@@ -68,6 +68,7 @@ class Update extends Action
|
||||
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE)), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
|
||||
->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
|
||||
->param('enabled', true, new Boolean(), 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true)
|
||||
->param('purge', false, new Boolean(true), 'When true, purge all cached list responses for this collection as part of the update. Use this to force readers to see fresh data immediately instead of waiting for the cache TTL to expire.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('getDatabasesDB')
|
||||
@@ -76,7 +77,7 @@ class Update extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, ?string $name, ?array $permissions, bool $documentSecurity, bool $enabled, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, ?string $name, ?array $permissions, bool $documentSecurity, bool $enabled, bool $purge, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Authorization $authorization): void
|
||||
{
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
@@ -99,8 +100,6 @@ class Update extends Action
|
||||
// Map aggregate permissions into the multiple permissions they represent.
|
||||
$permissions = Permission::aggregate($permissions);
|
||||
|
||||
$enabled ??= $collection->getAttribute('enabled', true);
|
||||
|
||||
$collection = $dbForProject->updateDocument(
|
||||
'database_' . $database->getSequence(),
|
||||
$collectionId,
|
||||
@@ -119,6 +118,10 @@ class Update extends Action
|
||||
->setParam('databaseId', $databaseId)
|
||||
->setParam($this->getEventsParamKey(), $collection->getId());
|
||||
|
||||
if ($purge) {
|
||||
$this->purgeListCache($dbForProject, $collectionId);
|
||||
}
|
||||
|
||||
$this->addRowBytesInfo($collection, $dbForProject);
|
||||
|
||||
$response->dynamic($collection, $this->getResponseModel());
|
||||
|
||||
@@ -61,16 +61,16 @@ class Create extends Action
|
||||
$databaseKeys = System::getEnv('_APP_DATABASE_DOCUMENTSDB_KEYS', '');
|
||||
$databaseOverride = System::getEnv('_APP_DATABASE_DOCUMENTSDB_OVERRIDE');
|
||||
$dbScheme = System::getEnv('_APP_DB_HOST_DOCUMENTSDB', 'mongodb');
|
||||
$databaseSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''));
|
||||
$databaseSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', ''));
|
||||
$databaseSharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', '')));
|
||||
$databaseSharedTablesV1 = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', '')));
|
||||
break;
|
||||
case VECTORSDB:
|
||||
$databases = Config::getParam('pools-vectorsdb', []);
|
||||
$databaseKeys = System::getEnv('_APP_DATABASE_VECTORSDB_KEYS', '');
|
||||
$databaseOverride = System::getEnv('_APP_DATABASE_VECTORSDB_OVERRIDE');
|
||||
$dbScheme = System::getEnv('_APP_DB_HOST_VECTORSDB', 'postgresql');
|
||||
$databaseSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''));
|
||||
$databaseSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', ''));
|
||||
$databaseSharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', '')));
|
||||
$databaseSharedTablesV1 = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', '')));
|
||||
break;
|
||||
default:
|
||||
// legacy/tablesdb
|
||||
@@ -108,7 +108,7 @@ class Create extends Action
|
||||
if ($index !== false) {
|
||||
$selectedDsn = $databases[$index];
|
||||
} else {
|
||||
if (!empty($dsn)) {
|
||||
if (!empty($dsn) && !empty($databaseSharedTables)) {
|
||||
$beforeFilter = \array_values($databases);
|
||||
if ($isSharedTablesV1) {
|
||||
$databases = array_filter($databases, fn ($value) => \in_array($value, $databaseSharedTablesV1));
|
||||
@@ -118,7 +118,10 @@ class Create extends Action
|
||||
$databases = array_filter($databases, fn ($value) => !\in_array($value, $databaseSharedTables));
|
||||
}
|
||||
}
|
||||
$selectedDsn = !empty($databases) ? $databases[array_rand($databases)] : '';
|
||||
if (empty($databases)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, "No {$databasetype} database pool available for the current shared-tables mode");
|
||||
}
|
||||
$selectedDsn = $databases[array_rand($databases)];
|
||||
}
|
||||
|
||||
if (\in_array($selectedDsn, $databaseSharedTables)) {
|
||||
|
||||
@@ -103,6 +103,9 @@ class XList extends Action
|
||||
$os = $detector->getOS();
|
||||
$client = $detector->getClient();
|
||||
$device = $detector->getDevice();
|
||||
$deviceName = \is_array($device) ? ($device['deviceName'] ?? '') : '';
|
||||
$deviceBrand = \is_array($device) ? ($device['deviceBrand'] ?? '') : '';
|
||||
$deviceModel = \is_array($device) ? ($device['deviceModel'] ?? '') : '';
|
||||
|
||||
$output[$i] = new Document([
|
||||
'event' => $log['event'],
|
||||
@@ -110,6 +113,7 @@ class XList extends Action
|
||||
'userEmail' => $log['data']['userEmail'] ?? null,
|
||||
'userName' => $log['data']['userName'] ?? null,
|
||||
'mode' => $log['data']['mode'] ?? null,
|
||||
'userType' => $log['data']['userType'] ?? null,
|
||||
'ip' => $log['ip'],
|
||||
'time' => $log['time'],
|
||||
'osCode' => $os['osCode'],
|
||||
@@ -121,9 +125,9 @@ class XList extends Action
|
||||
'clientVersion' => $client['clientVersion'],
|
||||
'clientEngine' => $client['clientEngine'],
|
||||
'clientEngineVersion' => $client['clientEngineVersion'],
|
||||
'deviceName' => $device['deviceName'],
|
||||
'deviceBrand' => $device['deviceBrand'],
|
||||
'deviceModel' => $device['deviceModel'],
|
||||
'deviceName' => $deviceName,
|
||||
'deviceBrand' => $deviceBrand,
|
||||
'deviceModel' => $deviceModel,
|
||||
]);
|
||||
|
||||
$record = $geodb->get($log['ip']);
|
||||
|
||||
@@ -33,7 +33,7 @@ abstract class Action extends DatabasesAction
|
||||
return $this->databaseType.'.'.METRIC_DATABASE_ID_OPERATIONS_WRITES;
|
||||
}
|
||||
|
||||
public function setHttpPath(string $path): DatabasesAction
|
||||
public function setHttpPath(string $path): self
|
||||
{
|
||||
switch (true) {
|
||||
case str_contains($path, '/tablesdb'):
|
||||
@@ -50,7 +50,8 @@ abstract class Action extends DatabasesAction
|
||||
$this->databaseType = VECTORSDB;
|
||||
break;
|
||||
}
|
||||
return parent::setHttpPath($path);
|
||||
parent::setHttpPath($path);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -239,7 +239,7 @@ class Create extends Action
|
||||
}
|
||||
}
|
||||
|
||||
$transaction = $authorization->skip(fn () => $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $existing, $operations) {
|
||||
$transaction = $authorization->skip(fn () => $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $operations) {
|
||||
$dbForProject->createDocuments('transactionLogs', $staged);
|
||||
return $dbForProject->increaseDocumentAttribute(
|
||||
'transactions',
|
||||
|
||||
@@ -105,8 +105,7 @@ class Update extends Action
|
||||
* @throws Exception
|
||||
* @throws \Throwable
|
||||
* @throws \Utopia\Database\Exception
|
||||
* @throws Authorization
|
||||
* @throws Structure
|
||||
* @throws StructureException
|
||||
* @throws \Utopia\Http\Exception
|
||||
*/
|
||||
public function action(string $transactionId, bool $commit, bool $rollback, Document $project, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, Authorization $authorization, EventProcessor $eventProcessor): void
|
||||
@@ -183,19 +182,33 @@ class Update extends Action
|
||||
$dbForDatabases = $getDatabasesDB($databaseDoc);
|
||||
|
||||
try {
|
||||
$dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $queueForEvents, $usage, $queueForRealtime, $queueForFunctions, $queueForWebhooks, $authorization) {
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
|
||||
'status' => 'committing',
|
||||
])));
|
||||
$transaction = $authorization->skip(fn () => $dbForProject->updateDocument(
|
||||
'transactions',
|
||||
$transactionId,
|
||||
new Document(['status' => 'committing'])
|
||||
));
|
||||
|
||||
$operations = $authorization->skip(fn () => $dbForProject->find('transactionLogs', [
|
||||
Query::equal('transactionInternalId', [$transaction->getSequence()]),
|
||||
Query::orderAsc(),
|
||||
Query::limit(PHP_INT_MAX),
|
||||
]));
|
||||
$operations = $authorization->skip(fn () => $dbForProject->find('transactionLogs', [
|
||||
Query::equal('transactionInternalId', [$transaction->getSequence()]),
|
||||
Query::orderAsc(),
|
||||
Query::limit(PHP_INT_MAX),
|
||||
]));
|
||||
|
||||
$collections = [];
|
||||
foreach ($operations as $operation) {
|
||||
$databaseInternalId = $operation['databaseInternalId'];
|
||||
$collectionInternalId = $operation['collectionInternalId'];
|
||||
$collectionId = "database_{$databaseInternalId}_collection_{$collectionInternalId}";
|
||||
|
||||
if (!isset($collections[$collectionId])) {
|
||||
$collections[$collectionId] = $authorization->skip(
|
||||
fn () => $dbForProject->getCollection($collectionId)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$dbForDatabases->withTransaction(function () use ($dbForDatabases, $transactionState, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $collections) {
|
||||
$state = [];
|
||||
$collections = [];
|
||||
|
||||
foreach ($operations as $operation) {
|
||||
$databaseInternalId = $operation['databaseInternalId'];
|
||||
@@ -211,11 +224,6 @@ class Update extends Action
|
||||
$data = $data->getArrayCopy();
|
||||
}
|
||||
|
||||
if (!isset($collections[$collectionId])) {
|
||||
$collections[$collectionId] = $authorization->skip(
|
||||
fn () => $dbForProject->getCollection($collectionId)
|
||||
);
|
||||
}
|
||||
$collection = $collections[$collectionId];
|
||||
|
||||
if (\is_array($data) && !empty($data)) {
|
||||
@@ -277,16 +285,17 @@ class Update extends Action
|
||||
}
|
||||
}
|
||||
|
||||
$transaction = $authorization->skip(fn () => $dbForProject->updateDocument(
|
||||
'transactions',
|
||||
$transactionId,
|
||||
new Document(['status' => 'committed'])
|
||||
));
|
||||
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_DOCUMENT)
|
||||
->setDocument($transaction);
|
||||
});
|
||||
|
||||
$transaction = $authorization->skip(fn () => $dbForProject->updateDocument(
|
||||
'transactions',
|
||||
$transactionId,
|
||||
new Document(['status' => 'committed'])
|
||||
));
|
||||
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_DOCUMENT)
|
||||
->setDocument($transaction);
|
||||
} catch (NotFoundException $e) {
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
|
||||
'status' => 'failed',
|
||||
|
||||
+6
@@ -34,6 +34,12 @@ class Create extends DocumentCreate
|
||||
return UtopiaResponse::MODEL_DOCUMENT_LIST;
|
||||
}
|
||||
|
||||
protected function getSupportForEmptyDocument()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Logs;
|
||||
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Logs\XList as DocumentLogXList;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
|
||||
class XList extends DocumentLogXList
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'listDocumentsDBDocumentLogs';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents/:documentId/logs')
|
||||
->desc('List document logs')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'documents.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'documentsDB',
|
||||
group: 'logs',
|
||||
name: 'listDocumentLogs',
|
||||
description: '/docs/references/documentsdb/get-document-logs.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_OK,
|
||||
model: $this->getResponseModel(),
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON,
|
||||
))
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('collectionId', '', new UID(), 'Collection ID.')
|
||||
->param('documentId', '', new UID(), 'Document ID.')
|
||||
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('getDatabasesDB')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->inject('authorization')
|
||||
->inject('audit')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Logs;
|
||||
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Logs\XList as CollectionLogXList;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
|
||||
class XList extends CollectionLogXList
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'listDocumentsDBCollectionLogs';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/logs')
|
||||
->desc('List collection logs')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'collections.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'documentsDB',
|
||||
group: $this->getSdkGroup(),
|
||||
name: 'listCollectionLogs',
|
||||
description: '/docs/references/documentsdb/get-collection-logs.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_OK,
|
||||
model: $this->getResponseModel(),
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
|
||||
->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject'])
|
||||
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->inject('authorization')
|
||||
->inject('audit')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user