Compare commits

...
Author SHA1 Message Date
Jake BarnbyandGitHub f4af241804 Merge pull request #9157 from appwrite/delete-attribute-not-found
Delete attribute No throwing Exception on not found
2025-01-08 16:07:53 +13:00
Jake BarnbyandGitHub 077313fe2d Update src/Appwrite/Platform/Workers/Databases.php 2025-01-08 15:46:35 +13:00
Christy JacobandGitHub b64eb2df38 Merge pull request #9146 from appwrite/feat-rollback-specifications
Set base specification CPUs to 0.5 again
2025-01-07 12:34:23 +05:30
Eldad A. FuxandGitHub 3b9cbcb671 Merge pull request #9142 from appwrite/usage-db-read-writes-addition
Usage databases api read writes addition
2025-01-06 19:38:41 +01:00
shimon f2cbdbae87 added operations debug header 2025-01-06 19:32:19 +02:00
shimon 347ba4a127 Merge branch '1.6.x' of https://github.com/appwrite/appwrite into usage-db-read-writes-addition 2025-01-06 19:30:20 +02:00
shimon 5c097df56a added operations debug header 2025-01-06 19:07:18 +02:00
shimon c97b48f443 Merge remote-tracking branch 'origin/usage-db-read-writes-addition' into usage-db-read-writes-addition
# Conflicts:
#	app/controllers/api/databases.php
2025-01-06 19:03:32 +02:00
shimon a481e958e3 added operations debug header 2025-01-06 18:57:35 +02:00
fogelito 40b407027e Count changes 2025-01-06 18:51:39 +02:00
shimon b910d795f2 Merge remote-tracking branch 'origin/usage-db-read-writes-addition' into usage-db-read-writes-addition 2025-01-06 18:06:29 +02:00
shimon d64ea97ee6 added operations debug header 2025-01-06 18:06:18 +02:00
fogelito fc03075ef8 Update counts 2025-01-06 18:01:44 +02:00
shimon f70222d41a added operations debug header 2025-01-06 17:19:39 +02:00
shimon 1cfcd53ae3 Merge branch 'usage-db-read-writes-addition' of https://github.com/appwrite/appwrite into usage-db-read-writes-addition 2025-01-06 16:50:54 +02:00
shimon 0814fe49e2 added operations debug header 2025-01-06 16:50:23 +02:00
Eldad A. FuxandGitHub eb8ca0229a Merge pull request #9177 from appwrite/PLA-2158
feat(swoole): allow configuration override of available cpus
2025-01-06 14:18:15 +01:00
Fabian Gruber 1bc419b6a1 feat(swoole): allow configuration override of available cpus 2025-01-06 13:48:01 +01:00
fogelito 895e79e736 Operations 2025-01-06 12:25:56 +02:00
shimon 1a73847c47 added operations debug header 2025-01-06 11:28:26 +02:00
Christy JacobandGitHub b680c5021a Merge pull request #9155 from appwrite/christyjacob4-patch-1
Update general.php
2025-01-04 12:21:17 +05:30
fogelito e8e39dbd56 Revert lock 2025-01-02 17:57:46 +02:00
fogelito bd083ac280 database 0.53.29 2025-01-02 17:54:47 +02:00
fogelito 06a5876f35 No throwing Exception 2025-01-02 17:06:22 +02:00
shimon 4acc2736e4 added operations debug header 2025-01-02 16:46:38 +02:00
shimon c5a49cc537 added operations debug header 2025-01-02 16:45:21 +02:00
Christy JacobandGitHub bdb93cd323 Update general.php 2025-01-02 19:03:28 +05:30
shimon 6b45293f78 file transformations usage 2025-01-02 13:55:30 +02:00
shimon 63cf093020 file transformations usage 2025-01-02 13:53:57 +02:00
shimon 504b7276cb composer 2025-01-02 11:02:53 +02:00
shimon e7e095907c composer 2025-01-02 10:44:56 +02:00
shimon c0d48efe18 composer 2025-01-02 10:29:55 +02:00
shimon fbbc9312ea database crud usage addition 2025-01-02 10:09:44 +02:00
shimon 4cc963d296 Merge branch '1.6.x' of https://github.com/appwrite/appwrite into usage-db-read-writes-addition 2025-01-02 10:00:59 +02:00
shimon c7841bb36e database crud usage addition 2025-01-02 09:59:59 +02:00
shimon 42589de95e database crud usage addition 2025-01-02 09:56:14 +02:00
shimon 0f2985cec1 database crud usage addition 2025-01-02 09:19:09 +02:00
Christy JacobandGitHub a08c71d9d2 Merge pull request #9112 from appwrite/chore-release-client-sdks
chore: release client sdks
2024-12-31 11:43:59 +05:30
Bradley Schofield d3b764a9e2 Set base specification CPUs to 0.5 again 2024-12-30 15:10:27 +00:00
loks0n 8c75203298 chore: bump sdk generator 2024-12-30 11:59:19 +00:00
loks0n dd19936fa5 Merge branch '1.6.x' of https://github.com/appwrite/appwrite into chore-release-client-sdks 2024-12-30 11:55:52 +00:00
loks0n dd0658de19 chore: bump versions 2024-12-30 11:26:11 +00:00
Eldad A. FuxandGitHub 0ab86f4dac Merge pull request #9144 from appwrite/feat-remove-firebase-oauth
Remove firebase OAuth API endpoints
2024-12-30 11:06:25 +01:00
Bradley Schofield 26bac5a2ea Remove firebase OAuth API endpoints 2024-12-30 09:00:52 +00:00
shimon dd13e8b3cf database crud usage addition 2024-12-29 18:02:19 +02:00
shimon 8e17fc69b3 database crud usage addition 2024-12-29 17:57:29 +02:00
shimon 025c9aeeb2 database crud usage addition 2024-12-29 17:53:45 +02:00
shimon c23af8168b database crud usage addition 2024-12-29 17:48:05 +02:00
shimon 58fd0d35bd database crud usage addition 2024-12-29 17:47:52 +02:00
Eldad A. FuxandGitHub daad365667 Merge pull request #9139 from appwrite/fix-remove-abuse-delete-trigger
fix: remove abuse delete trigger
2024-12-27 22:52:52 +01:00
loks0n 4691407a7a chore: bump utopia-php/abuse to 0.46.0 2024-12-27 20:41:30 +00:00
loks0n 769772a0a8 fix: remove abuse delete trigger 2024-12-27 20:30:03 +00:00
Eldad A. FuxandGitHub 8025082464 Merge pull request #9137 from appwrite/chore-remove-abuse-cleanup
chore remove abuse cleanup
2024-12-27 18:27:37 +01:00
loks0n 53860d2b96 chore: remove abuse cleanup 2024-12-27 10:07:53 +00:00
Eldad A. FuxandGitHub 84e5a5f6cb Merge pull request #9125 from appwrite/webhooks-usage-addition
Added webhooks usage stats
2024-12-24 15:18:42 +01:00
shimon 452a58de94 Reset matrics after enqueueing job via usage event 2024-12-24 15:38:01 +02:00
shimon dae5bf593c add webhooks usage stats 2024-12-24 11:09:41 +02:00
shimon 4b033d8f00 add webhooks usage stats 2024-12-24 10:50:09 +02:00
shimon fe19b0205a add webhooks usage stats 2024-12-23 19:44:10 +02:00
Christy JacobandGitHub 85cbc17996 Merge pull request #9124 from appwrite/migrate-to-redis-abuse
Migrate to redis abuse
2024-12-23 02:29:35 +05:30
Christy Jacob cddace70a4 fix: deletes worker abuse retention type 2024-12-23 01:45:57 +05:30
Christy Jacob 1935c7371a Merge branch '1.6.x' of github.com:appwrite/appwrite into migrate-to-redis-abuse 2024-12-23 01:30:28 +05:30
Christy Jacob 25953732ab fix: deletes worker abuse retention type 2024-12-23 01:30:20 +05:30
Christy JacobandGitHub ef032a7af0 Merge pull request #9121 from appwrite/migrate-to-redis-abuse
chore: use redis adapter for abuse
2024-12-20 22:26:28 +05:30
Christy Jacob e9c8766a23 chore: update abuse library 2024-12-20 21:53:12 +05:30
Christy Jacob 50601bd454 chore: use redis adapter for abuse 2024-12-20 20:14:50 +05:30
Christy JacobandGitHub 0715a8450a Merge pull request #9118 from appwrite/chore-optimise-webhooks-payload
chore: fix attribute name
2024-12-19 21:42:13 +05:30
Christy Jacob e2bdb46d48 chore: fix attribute name 2024-12-19 21:17:27 +05:30
Christy Jacob 718edd37f7 chore: fix attribute name 2024-12-19 21:15:13 +05:30
Christy JacobandGitHub ea4d41d929 Merge pull request #9117 from appwrite/revert-9111-migrate-to-redis-abuse
Revert "chore: initial commit"
2024-12-19 20:28:09 +05:30
Christy JacobandGitHub b97dea7996 Revert "chore: initial commit" 2024-12-19 20:27:54 +05:30
Christy JacobandGitHub 610121d02c Merge pull request #9115 from appwrite/chore-optimise-webhooks-payload
chore: optimise webhooks payload
2024-12-19 17:33:24 +05:30
Christy Jacob 6ec640cfd5 chore: linter 2024-12-19 16:56:17 +05:30
Christy Jacob d03d3c05b4 chore: optimise webhooks payload 2024-12-19 16:52:10 +05:30
Christy Jacob 53d8e2b0c3 chore: optimise webhooks payload 2024-12-19 16:43:05 +05:30
Christy JacobandGitHub 153431246d Merge pull request #9111 from appwrite/migrate-to-redis-abuse
chore: initial commit
2024-12-18 20:59:40 +05:30
Christy Jacob 12cb3b10bd chore: update abuse library 2024-12-18 20:42:29 +05:30
Christy Jacob 5a9dbe9c80 chore: fix realtime test 2024-12-18 20:06:24 +05:30
Christy Jacob d0f04126f3 chore: add missing resources to workers 2024-12-18 19:59:49 +05:30
Christy Jacob 599cc5e19a chore: review comments 2024-12-18 15:57:04 +05:30
Luke B. SilverandGitHub 0e56743dca Merge branch '1.6.x' into chore-release-client-sdks 2024-12-18 10:10:18 +00:00
Christy Jacob 77a8c27153 chore: fix realtime tests 2024-12-18 13:26:31 +05:30
Christy Jacob 911edd9457 chore: linter 2024-12-18 13:08:17 +05:30
Christy Jacob abbb2b544e Merge branch 'migrate-to-redis-abuse' of github.com:appwrite/appwrite into migrate-to-redis-abuse 2024-12-18 12:58:12 +05:30
Christy Jacob f1a089f8c6 feat: fix tests 2024-12-18 12:57:52 +05:30
Christy JacobandGitHub e8a3b87f68 Merge branch '1.6.x' into migrate-to-redis-abuse 2024-12-18 12:46:09 +05:30
Christy Jacob 01393faf0b feat: update abuse library 2024-12-18 11:58:34 +05:30
Christy JacobandGitHub a42fa741b7 Merge pull request #9109 from appwrite/fix-project-accessed-at
Fix: update project accessed at from router and schedulers
2024-12-18 09:56:19 +05:30
Damodar Lohani e24cce4e48 refactor 2024-12-17 14:19:26 +00:00
Damodar Lohani d7d69b63a8 reset worker 2024-12-17 14:15:27 +00:00
Damodar Lohani 2fb9ebe71d update project accessed at in schedules 2024-12-17 14:05:49 +00:00
Damodar Lohani 0cdb2a98be fix db name 2024-12-17 14:05:37 +00:00
Damodar Lohani c7713f58dd remove worker hook 2024-12-17 13:52:58 +00:00
loks0n 47785d9c4d docs: update 2024-12-17 11:58:25 +00:00
loks0n 6372cf59cc chore: composer 2024-12-17 11:54:50 +00:00
Christy Jacob 52ac358f53 chore: rename variable 2024-12-17 10:23:52 +05:30
Christy Jacob cc867e2859 chore: rename variable 2024-12-17 10:23:09 +05:30
Christy Jacob b55a01c5b3 chore: linter 2024-12-17 10:12:19 +05:30
Christy Jacob ed918e1748 Merge branch '1.6.x' of github.com:appwrite/appwrite into migrate-to-redis-abuse 2024-12-17 10:09:00 +05:30
Christy Jacob 801f311faf feat: cleanup 2024-12-17 10:08:16 +05:30
Christy Jacob 67996a8a14 feat: cleanup 2024-12-17 10:00:45 +05:30
Christy Jacob e8cea70346 feat: cleanup 2024-12-17 10:00:28 +05:30
Damodar Lohani 06c09503df update project rsource 2024-12-17 04:27:28 +00:00
Christy Jacob ee05f3d3bc feat: migrate to redis abuse 2024-12-17 09:49:31 +05:30
Damodar LohaniandGitHub f785602a4d Merge branch '1.6.x' into fix-project-accessed-at 2024-12-17 10:02:20 +05:45
Christy Jacob 396faa56c2 chore: initial commit 2024-12-17 08:21:38 +05:30
Christy JacobandGitHub 8759330896 Merge pull request #9100 from appwrite/feat-upgrade-assistant
feat: upgrade assistant
2024-12-16 19:30:13 +05:30
loks0n a24dc79036 chore: fix endpoint 2024-12-16 12:36:02 +00:00
Damodar Lohani ab4a444154 Fix: project accessed at on workers 2024-12-16 06:40:53 +00:00
Damodar Lohani 515b5904b8 update project accessed at from router 2024-12-16 06:39:25 +00:00
Luke B. SilverandGitHub ac03a72610 Merge branch '1.6.x' into feat-upgrade-assistant 2024-12-13 10:17:34 +00:00
Christy JacobandGitHub 9c2b78e612 Merge pull request #9102 from appwrite/feat-create-function-abuse-fix
Feat: createFunction abuse labels
2024-12-13 01:10:34 +04:00
Matej Bačo 905b4938a4 Fix failing test 2024-12-12 20:55:33 +00:00
Matej Bačo 8b3f09a981 Env var for function abuse limit 2024-12-12 21:10:22 +01:00
Christy JacobandGitHub a148b624b0 Merge pull request #9104 from appwrite/fix-usage-payload-bug
Usage payload debug
2024-12-13 00:03:06 +04:00
Matej Bačo fcd991fff2 Merge branch 'feat-create-function-abuse-fix' of github.com:appwrite/appwrite into feat-create-function-abuse-fix 2024-12-12 21:00:19 +01:00
Matej Bačo ab7ea056e6 Increase limit 2024-12-12 21:00:04 +01:00
shimon 434aa6d140 Merge branch '1.6.x' of https://github.com/appwrite/appwrite into fix-usage-payload-bug 2024-12-12 21:58:33 +02:00
shimon 01019f5055 Usage payload debug 2024-12-12 21:54:59 +02:00
Matej BačoandGitHub 5a0369ce27 Merge branch '1.6.x' into feat-create-function-abuse-fix 2024-12-12 20:52:39 +01:00
Matej Bačo 04139d6901 Linter fix 2024-12-12 20:52:31 +01:00
Matej Bačo 9c4d0f2a79 Leftover 2024-12-12 20:48:28 +01:00
Matej Bačo a18472507b Add createFunction abuse check + fix abuse 2024-12-12 20:45:49 +01:00
Christy JacobandGitHub 06adb3bcab Merge pull request #9103 from appwrite/fix-usage-payload-bug
Usage payload debug
2024-12-12 23:31:58 +04:00
shimon 177fd92094 Usage payload debug 2024-12-12 21:20:55 +02:00
loks0n bc38fd2640 fix: hide system prompt 2024-12-12 19:04:31 +00:00
loks0n eb2c3cd73f chore: platform bump 2024-12-12 18:55:38 +00:00
Christy JacobandGitHub 796b06fec5 Merge pull request #9101 from appwrite/fix-usage-payload-bug
Usage payload debug
2024-12-12 22:48:50 +04:00
Matej Bačo 6a9eeeb370 Add createFunction abuse labels 2024-12-12 19:39:24 +01:00
shimon 717b06950e Usage payload debug 2024-12-12 20:20:47 +02:00
loks0n a12b758658 feat: upgrade assistant to 0.6,0 2024-12-12 17:11:22 +00:00
Christy JacobandGitHub ef150ac2c0 Merge pull request #9099 from appwrite/fix-usage-payload-bug
Fix usage payload bug
2024-12-12 20:58:12 +04:00
shimon 158954df0b Usage payload debug 2024-12-12 18:55:45 +02:00
shimon c5605ac8be payload bug fix 2024-12-12 17:46:27 +02:00
shimon dc64fe85e9 payload bug fix 2024-12-12 17:46:04 +02:00
Christy JacobandGitHub 653db9d7ad Merge pull request #9098 from appwrite/fix-usage-payload-bug
payload bug fix
2024-12-12 18:57:55 +04:00
shimon 6a28d4eee6 payload bug fix 2024-12-12 16:55:14 +02:00
shimon f6157dd2c0 payload bug fix 2024-12-12 16:44:41 +02:00
Christy JacobandGitHub 9a693ecfb5 Merge pull request #9055 from appwrite/PLA-1990
fix(realtime): decrement connectionCounter only if connection is known
2024-12-12 18:06:32 +04:00
Christy JacobandGitHub 3949ce5f6b Merge pull request #9096 from appwrite/refactor-db-for-console
chore: replace occurrences of dbForConsole to dbForPlatform
2024-12-12 15:15:19 +04:00
Christy JacobandGitHub c2371e14f2 Merge pull request #9097 from appwrite/fix-usage-payload-bug
Fix usage payload bug
2024-12-12 14:37:10 +04:00
shimon ed66e8e41b payload bug fix 2024-12-12 12:36:34 +02:00
shimon 6dcd679412 payload bug fix 2024-12-12 12:34:39 +02:00
shimon b145477114 payload bug fix 2024-12-12 12:30:36 +02:00
Christy Jacob 75b42e134a chore: replace occurrences of dbForConsole to dbForPlatform 2024-12-12 14:30:26 +04:00
Jake BarnbyandGitHub 0c3ed7f81b Merge pull request #9092 from appwrite/migration-throw-error
Migration throw error
2024-12-12 20:01:30 +13:00
Christy JacobandGitHub 3b93b33dc9 Merge pull request #9076 from appwrite/fix-dont-checkout-git-history
feat: delete git folder
2024-12-11 19:11:04 +04:00
fogelito 17b5b3cf4b Remove comments 2024-12-11 17:06:19 +02:00
fogelito 4984abb0dd tests nulls 2024-12-11 16:58:00 +02:00
fogelito 66c5f959c0 Sentry Console::error 2024-12-11 16:34:13 +02:00
fogelito 142782825d Add throwable migration error 2024-12-11 13:31:17 +02:00
fogelito 071f050aaf Add throwable migration error 2024-12-11 12:57:25 +02:00
loks0n 13157837ec Merge branch '1.6.x' of https://github.com/appwrite/appwrite into fix-dont-checkout-git-history 2024-12-10 13:27:54 +00:00
Christy JacobandGitHub 203e11053f Merge pull request #9085 from appwrite/update-usage-payload
Update usage-dump payload
2024-12-10 07:22:29 -06:00
shimon 834c237cd5 handling map size 2024-12-10 15:18:21 +02:00
loks0n f2027bad2c chore: bump composer 2024-12-10 13:12:20 +00:00
shimon 5ffb81df43 update usage project attr 2024-12-09 17:42:05 +02:00
loks0n 2c87e3f4e5 feat: delete git folder 2024-12-06 13:51:24 +00:00
Jake BarnbyandGitHub dfb5ff1feb Merge pull request #9066 from appwrite/chore-upgrade-utopia-storage
Chore: upgrade utopia storage
2024-12-05 21:00:07 +13:00
Damodar Lohani 968b16d8bf upgrade utopia storage 2024-12-04 08:38:26 +00:00
Fabian Gruber 6e4b365992 fix(realtime): decrement connectionCounter only if connection is known 2024-12-02 10:39:38 +01:00
Jake BarnbyandGitHub 3ce63b3d7d Merge pull request #9038 from appwrite/1.6.x-fix-delete-attribute
Fix delete notFound attribute
2024-12-02 13:49:42 +13:00
Steven NguyenandGitHub 3954560e30 Merge pull request #9050 from appwrite/1.6.x-db-0.53.20.a
Bomb db 0.53.200
2024-12-01 00:43:27 -08:00
fogelito 8349a7e8f7 lock 2024-12-01 10:17:28 +02:00
fogelito b923893699 0.53.20.hotfix1 2024-12-01 10:11:19 +02:00
Christy JacobandGitHub 16b5a8507e Merge pull request #9037 from appwrite/lohanidamodar-patch-4
Update storage.php
2024-11-28 12:10:41 +01:00
Christy JacobandGitHub 992808f640 Merge pull request #9039 from appwrite/fix-rules-collection
feat: use environment variable to check rules format
2024-11-28 11:49:50 +01:00
Christy Jacob 4dbf11e236 feat: review comments and linter 2024-11-28 14:33:02 +04:00
Christy Jacob 48d20b2c67 feat: update proxy and certificates files 2024-11-28 14:18:37 +04:00
Christy Jacob 7e98493324 feat: use environment variable to check rules format 2024-11-28 14:08:26 +04:00
fogelito 1054f877a1 Merge branch '1.6.x' of https://github.com/appwrite/appwrite into 1.6.x-fix-delete-attribute 2024-11-28 12:07:40 +02:00
Damodar LohaniandGitHub a4ff1bf3be Update storage.php 2024-11-28 15:47:09 +05:45
fogelito 6be15eee0c try catch not found 2024-11-28 11:59:43 +02:00
Jake BarnbyandGitHub 3d44757e37 Merge pull request #9032 from appwrite/1.6.x-fix-delete-attribute
Remove failed attribute
2024-11-28 16:32:32 +13:00
Jake Barnby 856be8ce0f Merge remote-tracking branch 'origin/1.6.x' into 1.6.x-fix-delete-attribute 2024-11-28 16:14:23 +13:00
Steven NguyenandGitHub 91be70babf Merge pull request #9034 from appwrite/chore-bump-console-5.2.27
Bump console to version 5.2.27
2024-11-27 09:48:39 -08:00
Steven NguyenandGitHub c062a8f129 Bump console to version 5.2.27 2024-11-27 16:47:32 +00:00
Steven NguyenandGitHub 35005657b9 Merge pull request #9026 from appwrite/feat-1.6.1-release-prep
Prepare 1.6.1 release
2024-11-27 08:44:36 -08:00
fogelito 98864b04e1 fix $options 2024-11-27 18:02:37 +02:00
fogelito e39c96e6d1 Merge branch '1.6.x' of https://github.com/appwrite/appwrite into 1.6.x-fix-delete-attribute 2024-11-27 14:37:36 +02:00
fogelito ca6cd349fb Remove failed attribute 2024-11-27 14:35:58 +02:00
Christy JacobandGitHub 441e56f445 Merge pull request #8932 from appwrite/fix-preview-domains-development
Fix: Preview domains locally
2024-11-27 13:13:33 +01:00
Christy JacobandGitHub 92faa24d38 Merge pull request #9028 from appwrite/PLA-1913
feat: add more tags to logging
2024-11-26 16:43:46 +01:00
Christy JacobandGitHub 399261bfb1 Merge pull request #9029 from appwrite/1.6.x-fix-restoration
fix restoration
2024-11-26 16:40:38 +01:00
Fabian Gruber f0b6c729c9 feat: add more tags to sentry 2024-11-26 16:13:55 +01:00
fogelito 135ff1a724 bomb migration 0.6.13 2024-11-26 17:05:17 +02:00
fogelito 442459f6a4 fix restoration 2024-11-26 16:14:38 +02:00
Christy JacobandGitHub 74c8a664a8 Merge pull request #8838 from appwrite/PLA-1791
feat: move certificate generation to Adapter
2024-11-26 14:24:12 +01:00
Fabian Gruber 98984a8085 refactor: use single adapter 2024-11-26 13:54:52 +01:00
Fabian Gruber 3a9ba8a6ad feat: move certificate generation to Adapter 2024-11-26 13:54:52 +01:00
Christy JacobandGitHub ce35567c05 Merge pull request #9002 from appwrite/fix-project-accessed-at
Fix: update project last activity on init
2024-11-26 13:18:55 +01:00
Damodar Lohani 3a21ed6333 Merge remote-tracking branch 'origin/1.6.x' into fix-project-accessed-at 2024-11-26 11:51:50 +00:00
Damodar Lohani 580a9f331f fix user update 2024-11-26 08:59:03 +00:00
Christy JacobandGitHub 33fad2cb23 Merge pull request #9020 from appwrite/chore-bump-console-5.2.25
Bump console to version 5.2.25
2024-11-25 17:24:25 +01:00
Christy JacobandGitHub 533009c1da Merge branch '1.6.x' into chore-bump-console-5.2.25 2024-11-25 17:21:52 +01:00
Steven NguyenandGitHub cb385674a5 Add 1.6.1 to CHANGES.md 2024-11-25 15:58:22 +00:00
Steven NguyenandGitHub fb4c71aebb Merge pull request #9025 from appwrite/fix-tasks-warning
Fix: swoole task warning
2024-11-25 07:07:09 -08:00
Matej Bačo 9d3bc1f80a Fix swoole task warning 2024-11-25 11:04:04 +01:00
Steven NguyenandGitHub 8da2b249b7 Merge pull request #9019 from appwrite/fix-8569-users-create-session-secret
Fix users create session secret
2024-11-22 21:55:28 -08:00
Steven NguyenandGitHub 07b3cbaec9 Bump appwrite version to 1.6.1 2024-11-22 22:47:02 +00:00
Steven NguyenandGitHub 98d78163a2 Bump console to version 5.2.25 2024-11-22 22:39:24 +00:00
Steven NguyenandGitHub 014c613c93 fix: update secret returned from users.createSession()
1. Include at least 1 factor because the minumum number of factors
   required when mfa is disabled is 1.
2. Purge the cached user document to ensure the new session is included
   in subsequent requests for the user.
3. Fix the encoding of the secret to match other parts of the codebase.
2024-11-22 21:52:27 +00:00
Steven NguyenandGitHub 54ec39f513 Merge pull request #9016 from appwrite/feat-safe-workers
Feat: Safe workers
2024-11-22 11:07:18 -08:00
Steven NguyenandGitHub ad235610da Merge pull request #9018 from appwrite/rules-backwards-compatibility
chore: backwards compatibility for 1.6.x
2024-11-22 09:53:09 -08:00
Christy Jacob ae7cb36240 chore: linter 2024-11-22 17:18:23 +00:00
Christy Jacob 33dd4df694 chore: backwards compatibility for 1.6.x 2024-11-22 17:16:51 +00:00
Matej Bačo 2fcfd1d7c9 Add safe workers 2024-11-22 13:57:42 +00:00
Christy JacobandGitHub 7ccc74bae2 Merge pull request #9006 from appwrite/fix-team-invite-session-creation
fix: team invites with existing session
2024-11-22 09:38:40 +01:00
Christy JacobandGitHub 44a0469d18 Merge branch '1.6.x' into fix-team-invite-session-creation 2024-11-22 09:21:54 +01:00
Christy JacobandGitHub 34d8a0504b Merge pull request #9013 from appwrite/feat-db-create-on-server-start
Improved shared tables V2
2024-11-21 10:41:52 +01:00
Jake Barnby f9e518b2b0 Fix shared tables v2 deletes 2024-11-21 16:49:49 +13:00
Jake Barnby 8410710005 Remove redundant DSN construction 2024-11-21 16:47:12 +13:00
Jake Barnby 018c865d04 Skip all project db calls on project create if shared tables v2 2024-11-21 16:46:36 +13:00
Jake Barnby 415dedc135 Create V2 shared tables databases on server start 2024-11-21 16:46:10 +13:00
loks0nandJake Barnby d257fdb045 chore: remove redundant compression 2024-11-21 14:24:56 +13:00
Steven NguyenandJake Barnby f6e7c12b02 Merge pull request #8922 from appwrite/lohanidamodar-patch-4
Improve compression param checks
2024-11-21 14:17:52 +13:00
Christy JacobandJake Barnby 908adebf74 Merge pull request #8964 from appwrite/debug-whitelist-console
debug: whitelist console
2024-11-21 14:11:20 +13:00
Christy JacobandJake Barnby 6e6899c1e9 Merge pull request #8988 from appwrite/fix-deletes
Fix project type delete checks
2024-11-21 14:09:02 +13:00
Christy JacobandJake Barnby f490d4edaf Merge pull request #8797 from appwrite/feat-migration
Feat migration
# Conflicts:
#	composer.lock
2024-11-21 14:08:20 +13:00
Torsten Dittmann 0b11e0cc92 fix: gitignore merge conflict leftover 2024-11-20 11:52:27 +01:00
Torsten DittmannandGitHub a5d43ddf71 Merge pull request #9011 from appwrite/sync-1.6.x
sync: 1.6.x
2024-11-20 11:48:37 +01:00
Torsten Dittmann 15a5f8690d sync: 1.6.x with main 2024-11-20 11:33:38 +01:00
Torsten Dittmann 9901c781b7 Merge branch 'main' of https://github.com/appwrite/appwrite into sync-1.6.x 2024-11-20 11:31:52 +01:00
Damodar Lohani c33c5732e0 move accessed at to init 2024-11-20 04:29:57 +00:00
Damodar Lohani 2da2b5bb7f reset error hook 2024-11-20 04:25:37 +00:00
Torsten Dittmann c97a94ec98 chore: run formatter 2024-11-19 17:18:51 +01:00
Torsten Dittmann 5f56c45c6c fix: team invites with existing session 2024-11-19 17:18:51 +01:00
Eldad A. FuxandGitHub ee3d34310a Merge pull request #9005 from appwrite/base-0.9.5
chore: update base to 0.9.5
2024-11-19 17:15:43 +01:00
Fabian Gruber 877a978451 chore: update base to 0.9.5 2024-11-19 16:42:16 +01:00
Damodar Lohani e8079cfd29 lint 2024-11-19 06:59:30 +00:00
Damodar LohaniandGitHub 842e23220a update project last activity on error hook 2024-11-19 07:51:50 +05:45
Matej BačoandGitHub b2cd20bf29 Merge pull request #8934 from appwrite/fix-bandwidth-calculation
Fix: bandwidth calculation
2024-11-18 11:16:51 +01:00
Jake BarnbyandGitHub d49b0540fd Merge pull request #8978 from appwrite/unset-index-length
Unset index length
2024-11-18 17:05:00 +13:00
Torsten DittmannandGitHub 8e02115828 ci: console sdk previews (#8990) 2024-11-15 13:27:46 +01:00
Steven NguyenandGitHub 3a748fbd35 Merge pull request #8972 from appwrite/fix-remove-duplicate-dart-216-runtime-template
Remove duplicate dart-2.16 runtime template
2024-11-14 12:33:33 -08:00
fogelito 7e7a4658cd revert lock 2024-11-14 14:20:28 +02:00
fogelito abf97e391e formatting 2024-11-14 14:19:33 +02:00
fogelito d8799e5a9c Remove comment 2024-11-14 14:13:28 +02:00
fogelito 217e471a41 Merge branch '1.6.x' of https://github.com/appwrite/appwrite into unset-index-length 2024-11-14 14:12:03 +02:00
fogelito 72e491ac69 Remove unsetting index length for strings 2024-11-14 14:11:36 +02:00
Jake BarnbyandGitHub bdb0b188ef Merge pull request #8971 from appwrite/fix-alter-attributes
Fix Patch attribute
2024-11-14 20:55:59 +13:00
Jake BarnbyandGitHub 191f884772 Merge pull request #8959 from appwrite/fix-send-migration-errors-individually
Send migration errors individually
2024-11-14 19:57:27 +13:00
Steven NguyenandGitHub 45f85d6b4d fix: remove duplicate dart-2.16 runtime template 2024-11-13 11:30:48 -08:00
fogelito 8f845aef53 Merge branch '1.6.x' of https://github.com/appwrite/appwrite into fix-alter-attributes
# Conflicts:
#	src/Appwrite/Platform/Workers/Databases.php
2024-11-13 18:34:57 +02:00
fogelito 68e96e9851 comment 2024-11-13 18:32:20 +02:00
Eldad A. FuxandGitHub c6cf27bd6f Merge pull request #8960 from appwrite/PLA-1854
feat: add telemetry
2024-11-13 14:08:45 +01:00
fogelito 71f2c6c131 Remove purgeCachedCollection 2024-11-13 14:55:56 +02:00
Fabian Gruber 1db71a46ea chore: use telemetry 0.1.0 2024-11-13 13:52:41 +01:00
Fabian Gruber 8124f6998e feat: add telemetry 2024-11-13 13:52:41 +01:00
Torsten DittmannandGitHub 7f06b0ec76 Merge pull request #8969 from appwrite/fix-memberships-privacy-mfa
fix: memberships privacy mfa
2024-11-13 12:36:57 +01:00
loks0n ca533f40c9 fix: memberships-privacy mfa only 2024-11-13 10:45:50 +01:00
Bradley Schofield 54db5655a1 Run Linter 2024-11-13 13:42:49 +09:00
Bradley Schofield 91cc0e627a Address Comments 2024-11-13 10:29:43 +09:00
Bradley Schofield f1af8b330e Merge remote-tracking branch 'origin/1.6.x' into fix-send-migration-errors-individually 2024-11-13 09:13:15 +09:00
Bradley Schofield 29e7d1f7d0 Address Comments 2024-11-13 09:13:00 +09:00
Steven NguyenandGitHub 97bcccba85 Merge pull request #8963 from appwrite/remove-inaccurate-doc
Remove inaccurate info about leaving the URL parameter empty
2024-11-12 15:43:56 -08:00
Ebenezer DonandGitHub 7f3a342940 Remove inaccurate info about leaving the URL parameter empty 2024-11-12 16:44:20 +00:00
Torsten DittmannandGitHub cca5606b70 Merge pull request #8943 from appwrite/feat-realtime-ping-pong
feat: realtime ping pong
2024-11-12 14:09:58 +01:00
Bradley Schofield 1337ea77b1 Fix Optionals 2024-11-12 16:18:09 +09:00
Bradley Schofield a460a41fe5 Run Linter 2024-11-12 11:33:13 +09:00
Bradley Schofield b62640489d Fire logs for each individual migration error 2024-11-12 11:29:19 +09:00
fogelito bae6e465fa updateAttributes 2024-11-11 18:46:47 +02:00
Torsten Dittmann cddd16b753 Merge branch '1.6.x' of https://github.com/appwrite/appwrite into feat-realtime-ping-pong 2024-11-07 13:10:23 +01:00
Torsten Dittmann f91f382baf style: remove unnecessary comment 2024-11-07 12:08:11 +01:00
Torsten Dittmann 4ee9e9730f feat: realtime ping pong 2024-11-07 12:05:37 +01:00
Matej Bačo c9e6ef8202 Rename based on PR reviews 2024-11-06 16:05:58 +00:00
Matej Bačo 87cfd7df3b Remove leftover 2024-11-06 11:39:44 +00:00
Matej Bačo ca887f018e fix domain execution response size 2024-11-06 11:29:24 +00:00
Matej Bačo 54c24b24e6 Improve preview domains local development flow 2024-11-06 10:52:43 +00:00
89 changed files with 4583 additions and 3434 deletions
+2
View File
@@ -23,6 +23,7 @@ _APP_OPENSSL_KEY_V1=your-secret-key
_APP_DOMAIN=traefik
_APP_DOMAIN_FUNCTIONS=functions.localhost
_APP_DOMAIN_TARGET=localhost
_APP_RULES_FORMAT=md5
_APP_REDIS_HOST=redis
_APP_REDIS_PORT=6379
_APP_REDIS_PASS=
@@ -108,3 +109,4 @@ _APP_MESSAGE_EMAIL_TEST_DSN=
_APP_MESSAGE_PUSH_TEST_DSN=
_APP_WEBHOOK_MAX_FAILED_ATTEMPTS=10
_APP_PROJECT_REGIONS=default
_APP_FUNCTIONS_CREATION_ABUSE_LIMIT=5000
+33
View File
@@ -0,0 +1,33 @@
name: "Console SDK Preview"
on:
pull_request:
paths:
- 'app/config/specs/*-latest-console.json'
jobs:
setup:
name: Setup & Build Console SDK
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Load and Start Appwrite
run: |
docker compose build
docker compose up -d
docker compose exec appwrite sdks --platform=console --sdk=web --version=latest --git=no
sudo chown -R $USER:$USER ./app/sdks/console-web
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Build and Publish SDK
working-directory: ./app/sdks/console-web
run: |
npm install
npm run build
npx pkg-pr-new publish --comment=update
+25 -5
View File
@@ -127,6 +127,11 @@ jobs:
Messaging,
Migrations
]
tables-mode: [
'Project',
'Shared V1',
'Shared V2',
]
steps:
- name: checkout
@@ -145,11 +150,26 @@ jobs:
docker compose up -d
sleep 30
- name: Run ${{matrix.service}} Tests
run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug
- name: Run ${{matrix.service}} Shared Tables Tests
run: _APP_DATABASE_SHARED_TABLES=database_db_main docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug
- name: Run ${{ matrix.service }} tests with ${{ matrix.tables-mode }} table mode
run: |
if [ "${{ matrix.tables-mode }}" == "Shared V1" ]; then
echo "Using shared tables V1"
export _APP_DATABASE_SHARED_TABLES=database_db_main
export _APP_DATABASE_SHARED_TABLES_V1=database_db_main
elif [ "${{ matrix.tables-mode }}" == "Shared V2" ]; then
echo "Using shared tables V2"
export _APP_DATABASE_SHARED_TABLES=database_db_main
export _APP_DATABASE_SHARED_TABLES_V1=
else
echo "Using project tables"
export _APP_DATABASE_SHARED_TABLES=
export _APP_DATABASE_SHARED_TABLES_V1=
fi
docker compose exec -T \
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug
benchmarking:
name: Benchmark
+1
View File
@@ -16,3 +16,4 @@ dev/yasd_init.php
.phpunit.result.cache
Makefile
appwrite.json
/.zed/
+114
View File
@@ -1,3 +1,117 @@
# Version 1.6.1
## What's Changed
### Notable changes
* Remove JPEG fallback for webp by @lohanidamodar in https://github.com/appwrite/appwrite/pull/8746
* Add heic and avif support by @lohanidamodar in https://github.com/appwrite/appwrite/pull/7718
* Add new runtimes by @Meldiron in https://github.com/appwrite/appwrite/pull/8771
* Remove audits deletion by @shimonewman in https://github.com/appwrite/appwrite/pull/8766
* Bump assistant by @loks0n in https://github.com/appwrite/appwrite/pull/8801
* Change max queries values to 500 by @fogelito in https://github.com/appwrite/appwrite/pull/8802
* Allow '.wav' as 'audio/x-wav' as well by @basert in https://github.com/appwrite/appwrite/pull/8846
* Use 1 instead of 0.5 cpu for default function specification by @loks0n in https://github.com/appwrite/appwrite/pull/8848
* Update function runtimes by @christyjacob4 in https://github.com/appwrite/appwrite/pull/8781
* Add a realtime heartbeat by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/8943
### Fixes
* Trigger functions event only if event is not paused by @lohanidamodar in https://github.com/appwrite/appwrite/pull/8526
* Update docker-compose to restart usage-dump by @feschaffa in https://github.com/appwrite/appwrite/pull/8642
* Fix typo in scheduler base by @fogelito in https://github.com/appwrite/appwrite/pull/8691
* Add domain and force HTTPS env vars to mail worker by @stnguyen90 in https://github.com/appwrite/appwrite/pull/8722
* Fix webp by @lohanidamodar in https://github.com/appwrite/appwrite/pull/8732
* Ignore junction tables by @fogelito in https://github.com/appwrite/appwrite/pull/8728
* Fix logger throwing fatal error by @lohanidamodar in https://github.com/appwrite/appwrite/pull/8724
* Fix missing protocol for testing SMTP by @byawitz in https://github.com/appwrite/appwrite/pull/8749
* Make create execution async loose by @loks0n in https://github.com/appwrite/appwrite/pull/8707
* Fix invalid cursor value by @fogelito in https://github.com/appwrite/appwrite/pull/8109
* Fix target deletes by @abnegate in https://github.com/appwrite/appwrite/pull/8833
* Fix translation commas by @loks0n in https://github.com/appwrite/appwrite/pull/8892
* Fix Migrations having source creds being overwritten and add Migration tests by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8897
* Fix validator usage for updating string size by @abnegate in https://github.com/appwrite/appwrite/pull/8890
* Fix create user event not triggering by @loks0n in https://github.com/appwrite/appwrite/pull/8718
* Improve error handling and logging in the database worker by @fogelito in https://github.com/appwrite/appwrite/pull/8944
* Remove inaccurate info about leaving the URL parameter empty by @ebenezerdon in https://github.com/appwrite/appwrite/pull/8963
* Ensure indexes are updated when updating an attribute key by @fogelito in https://github.com/appwrite/appwrite/pull/8971
* Remove duplicate dart-2.16 runtime template by @stnguyen90 in https://github.com/appwrite/appwrite/pull/8972
* Fix team invites with existing session by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/9006
* Improve handling of HTTP requests by dispatching to safe workers by @Meldiron in https://github.com/appwrite/appwrite/pull/9016
* Fix users create session secret by @stnguyen90 in https://github.com/appwrite/appwrite/pull/9019
* Fix swoole task warning by @Meldiron in https://github.com/appwrite/appwrite/pull/9025
### Miscellaneous
* Update Init copy by @adityaoberai in https://github.com/appwrite/appwrite/pull/8557
* Fix security scan permissions and comment by @EVDOG4LIFE in https://github.com/appwrite/appwrite/pull/8525
* Add Trivy security scans by @btme0011 in https://github.com/appwrite/appwrite/pull/6876
* Update database stack by @abnegate in https://github.com/appwrite/appwrite/pull/8564
* Bump database by @abnegate in https://github.com/appwrite/appwrite/pull/8573
* Sync main with 1.5.x by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8589
* Add AWS to one-click installs by @byawitz in https://github.com/appwrite/appwrite/pull/8593
* Update Init copy in readme by @adityaoberai in https://github.com/appwrite/appwrite/pull/8618
* Sync main into 1.6.x by @stnguyen90 in https://github.com/appwrite/appwrite/pull/8685
* Sync 1.6.x into main by @stnguyen90 in https://github.com/appwrite/appwrite/pull/8686
* Feat coroutines by @Meldiron in https://github.com/appwrite/appwrite/pull/7826
* Sync main into 1.6.x by @Meldiron in https://github.com/appwrite/appwrite/pull/8719
* Sentence casing endpoint API reference by @choir241 in https://github.com/appwrite/appwrite/pull/8617
* DB storage metrics by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8404
* Fix exception thrown when optional array attribute does not exist by @lohanidamodar in https://github.com/appwrite/appwrite/pull/8391
* Add projects channels to realtime by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/8735
* Base for console roles support by @lohanidamodar in https://github.com/appwrite/appwrite/pull/8565
* Remove DB disk storage calculation by @christyjacob4 in https://github.com/appwrite/appwrite/pull/8745
* Messaging adapter default values by @shimonewman in https://github.com/appwrite/appwrite/pull/8742
* Add payload response type by @loks0n in https://github.com/appwrite/appwrite/pull/8720
* Fix flaky functions tests by @loks0n in https://github.com/appwrite/appwrite/pull/8682
* Migrations Backups by @fogelito in https://github.com/appwrite/appwrite/pull/8186
* Add test for response and request filters by @vermakhushboo in https://github.com/appwrite/appwrite/pull/8697
* Bump version in SECURITY.md by @EVDOG4LIFE in https://github.com/appwrite/appwrite/pull/8755
* Add originalId attribute to databases collection by @fogelito in https://github.com/appwrite/appwrite/pull/8764
* Fix Walter References by @ItzNotABug in https://github.com/appwrite/appwrite/pull/8757
* Update database by @abnegate in https://github.com/appwrite/appwrite/pull/8769
* Move new attributes by @abnegate in https://github.com/appwrite/appwrite/pull/8777
* Add ping endpoint by @loks0n in https://github.com/appwrite/appwrite/pull/8761
* Fix GitHub action caching by @loks0n in https://github.com/appwrite/appwrite/pull/8772
* Chore release ruby SDK by @abnegate in https://github.com/appwrite/appwrite/pull/8767
* Call migration success on success by @abnegate in https://github.com/appwrite/appwrite/pull/8782
* Update utopia-php/system to 0.9.0 by @basert in https://github.com/appwrite/appwrite/pull/8780
* Move createDocument from api to worker by @vermakhushboo in https://github.com/appwrite/appwrite/pull/8776
* Add missing indexes by @christyjacob4 in https://github.com/appwrite/appwrite/pull/8803
* Update database by @abnegate in https://github.com/appwrite/appwrite/pull/8809
* Fix typo in BLR region by @stnguyen90 in https://github.com/appwrite/appwrite/pull/8756
* Add tests for project variables by @vermakhushboo in https://github.com/appwrite/appwrite/pull/8815
* Replace 'Expires' with 'Cache-Control: private' header to avoid CDN caching by @basert in https://github.com/appwrite/appwrite/pull/8836
* Allow blocking based on resource attributes by @basert in https://github.com/appwrite/appwrite/pull/8812
* Check if resource is blocked inside functions worker by @basert in https://github.com/appwrite/appwrite/pull/8855
* Fix missing allow attribute by @abnegate in https://github.com/appwrite/appwrite/pull/8889
* Revert function execution order by @basert in https://github.com/appwrite/appwrite/pull/8857
* Use resource type constants by @basert in https://github.com/appwrite/appwrite/pull/8895
* Update Database lib by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8680
* Update database by @abnegate in https://github.com/appwrite/appwrite/pull/8917
* Update database by @abnegate in https://github.com/appwrite/appwrite/pull/8923
* Update database for transaction counter fixes with retries by @abnegate in https://github.com/appwrite/appwrite/pull/8927
* Validate string permissions by @fogelito in https://github.com/appwrite/appwrite/pull/8929
* Add PubSub adapter support by @basert in https://github.com/appwrite/appwrite/pull/8905
* List memberships as client by @loks0n in https://github.com/appwrite/appwrite/pull/8913
* Fix XDebug Extension not being removed by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8891
* Update database by @abnegate in https://github.com/appwrite/appwrite/pull/8946
* Use utopia compression by @loks0n in https://github.com/appwrite/appwrite/pull/8938
* Make compression minimum size configurable by @loks0n in https://github.com/appwrite/appwrite/pull/8947
* Revert "Update database" by @christyjacob4 in https://github.com/appwrite/appwrite/pull/8949
* Fix setpaused by @loks0n in https://github.com/appwrite/appwrite/pull/8948
* Use getDocument instead of find() for rules by @christyjacob4 in https://github.com/appwrite/appwrite/pull/8951
* Remove double fetch from migrations worker by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8956
* Fix memberships privacy MFA by @loks0n in https://github.com/appwrite/appwrite/pull/8969
* Add telemetry by @basert in https://github.com/appwrite/appwrite/pull/8960
* Send migration errors individually by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8959
* Add console sdk previews by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/8990
* Unset index length by @fogelito in https://github.com/appwrite/appwrite/pull/8978
* Update base to 0.9.5 by @basert in https://github.com/appwrite/appwrite/pull/9005
* Sync main into 1.6.x by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/9011
* Improved shared tables V2 by @abnegate in https://github.com/appwrite/appwrite/pull/9013
* Ensure backwards compatibility for 1.6.x by @christyjacob4 in https://github.com/appwrite/appwrite/pull/9018
# Version 1.6.0
## What's Changed
+12
View File
@@ -613,6 +613,18 @@ If you need to clear the cache, you can do so by running the following command:
docker compose exec redis redis-cli FLUSHALL
```
## Using preview domains locally
Appwrite Functions are automatically given a domain you can visit to execute the function. This domain has format `[SOMETHING].functions.localhost` unless you changed `_APP_DOMAIN_FUNCTIONS` environment variable. This default value works great when running Appwrite locally, but it can be impossible to use preview domains with Cloud woekspaces such as Gitpod or GitHub Codespaces.
To use preview domains on Cloud workspaces, you can visit hostname provided by them, and supply function's preview domain as URL parameter:
```
https://8080-appwrite-appwrite-mjeb3ebilwv.ws-eu116.gitpod.io/ping?preview=672b3c7eab1ac523ccf5.functions.localhost
```
The path was set to `/ping` intentionally. Visiting `/` for preview domains might trigger Console background worker, and trigger redirect to Console without our preview URL param. Visiting different path ensures this doesnt happen.
## Tutorials
From time to time, our team will add tutorials that will help contributors find their way in the Appwrite source code. Below is a list of currently available tutorials:
+1 -1
View File
@@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
--no-plugins --no-scripts --prefer-dist \
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
FROM appwrite/base:0.9.3 AS final
FROM appwrite/base:0.9.5 AS final
LABEL maintainer="team@appwrite.io"
+3 -3
View File
@@ -67,7 +67,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.6.0
appwrite/appwrite:1.6.1
```
### Windows
@@ -79,7 +79,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.6.0
appwrite/appwrite:1.6.1
```
#### PowerShell
@@ -89,7 +89,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.6.0
appwrite/appwrite:1.6.1
```
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
+3 -3
View File
@@ -75,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.6.0
appwrite/appwrite:1.6.1
```
### Windows
@@ -87,7 +87,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.6.0
appwrite/appwrite:1.6.1
```
#### PowerShell
@@ -97,7 +97,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.6.0
appwrite/appwrite:1.6.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.
+14 -13
View File
@@ -52,7 +52,7 @@ CLI::setResource('pools', function (Registry $register) {
return $register->get('pools');
}, ['register']);
CLI::setResource('dbForConsole', function ($pools, $cache) {
CLI::setResource('dbForPlatform', function ($pools, $cache) {
$sleep = 3;
$maxAttempts = 5;
$attempts = 0;
@@ -67,9 +67,9 @@ CLI::setResource('dbForConsole', function ($pools, $cache) {
->pop()
->getResource();
$dbForConsole = new Database($dbAdapter, $cache);
$dbForPlatform = new Database($dbAdapter, $cache);
$dbForConsole
$dbForPlatform
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', 'console');
@@ -78,7 +78,7 @@ CLI::setResource('dbForConsole', function ($pools, $cache) {
$collections = Config::getParam('collections', [])['console'];
$last = \array_key_last($collections);
if (!($dbForConsole->exists($dbForConsole->getDatabase(), $last))) { /** TODO cache ready variable using registry */
if (!($dbForPlatform->exists($dbForPlatform->getDatabase(), $last))) { /** TODO cache ready variable using registry */
throw new Exception('Tables not ready yet.');
}
@@ -94,15 +94,15 @@ CLI::setResource('dbForConsole', function ($pools, $cache) {
throw new Exception("Console is not ready yet. Please try again later.");
}
return $dbForConsole;
return $dbForPlatform;
}, ['pools', 'cache']);
CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) {
CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) {
return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForConsole;
return $dbForPlatform;
}
try {
@@ -114,8 +114,9 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole,
if (isset($databases[$dsn->getHost()])) {
$database = $databases[$dsn->getHost()];
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -136,10 +137,10 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole,
->getResource();
$database = new Database($dbAdapter, $cache);
$databases[$dsn->getHost()] = $database;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -157,7 +158,7 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole,
return $database;
};
}, ['pools', 'dbForConsole', 'cache']);
}, ['pools', 'dbForPlatform', 'cache']);
CLI::setResource('queue', function (Group $pools) {
return $pools->get('queue')->pop()->getResource();
@@ -180,7 +181,7 @@ CLI::setResource('logError', function (Registry $register) {
$log = new Log();
$log->setNamespace($namespace);
$log->setServer(\gethostname());
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
+1 -1
View File
@@ -686,7 +686,7 @@ return [
],
Exception::ATTRIBUTE_LIMIT_EXCEEDED => [
'name' => Exception::ATTRIBUTE_LIMIT_EXCEEDED,
'description' => 'The maximum number of attributes has been reached.',
'description' => 'The maximum number or size of attributes for this collection has been reached.',
'code' => 400,
],
Exception::ATTRIBUTE_VALUE_INVALID => [
+1 -1
View File
@@ -11,7 +11,7 @@ const TEMPLATE_RUNTIMES = [
],
'DART' => [
'name' => 'dart',
'versions' => ['3.5', '3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16', '2.16']
'versions' => ['3.5', '3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16']
],
'GO' => [
'name' => 'go',
+5 -5
View File
@@ -11,7 +11,7 @@ return [
[
'key' => 'web',
'name' => 'Web',
'version' => '16.0.2',
'version' => '16.1.0',
'url' => 'https://github.com/appwrite/sdk-for-web',
'package' => 'https://www.npmjs.com/package/appwrite',
'enabled' => true,
@@ -59,7 +59,7 @@ return [
[
'key' => 'flutter',
'name' => 'Flutter',
'version' => '13.0.0',
'version' => '13.1.1',
'url' => 'https://github.com/appwrite/sdk-for-flutter',
'package' => 'https://pub.dev/packages/appwrite',
'enabled' => true,
@@ -77,7 +77,7 @@ return [
[
'key' => 'apple',
'name' => 'Apple',
'version' => '7.0.0',
'version' => '7.1.0',
'url' => 'https://github.com/appwrite/sdk-for-apple',
'package' => 'https://github.com/appwrite/sdk-for-apple',
'enabled' => true,
@@ -112,7 +112,7 @@ return [
[
'key' => 'android',
'name' => 'Android',
'version' => '6.0.0',
'version' => '6.1.0',
'url' => 'https://github.com/appwrite/sdk-for-android',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-android',
'enabled' => true,
@@ -134,7 +134,7 @@ return [
[
'key' => 'react-native',
'name' => 'React Native',
'version' => '0.5.0',
'version' => '0.6.0',
'url' => 'https://github.com/appwrite/sdk-for-react-native',
'package' => 'https://npmjs.com/package/react-native-appwrite',
'enabled' => true,
+1 -1
View File
@@ -6,7 +6,7 @@ return [
Specification::S_05VCPU_512MB => [
'slug' => Specification::S_05VCPU_512MB,
'memory' => 512,
'cpus' => 1 // TODO: revert this, it's a temporary change to test function performance.
'cpus' => 0.5
],
Specification::S_1VCPU_512MB => [
'slug' => Specification::S_1VCPU_512MB,
@@ -1,7 +1,7 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2784,7 +2784,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -5766,7 +5766,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 382,
"weight": 376,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5851,7 +5851,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 386,
"weight": 380,
"cookies": false,
"type": "",
"deprecated": false,
+72 -359
View File
@@ -1,7 +1,7 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2795,7 +2795,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -9488,7 +9488,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -10145,7 +10146,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -13673,7 +13675,7 @@
},
"x-appwrite": {
"method": "listMessages",
"weight": 390,
"weight": 384,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13751,7 +13753,7 @@
},
"x-appwrite": {
"method": "createEmail",
"weight": 387,
"weight": 381,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13897,7 +13899,7 @@
},
"x-appwrite": {
"method": "updateEmail",
"weight": 394,
"weight": 388,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14045,7 +14047,7 @@
},
"x-appwrite": {
"method": "createPush",
"weight": 389,
"weight": 383,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14202,7 +14204,7 @@
},
"x-appwrite": {
"method": "updatePush",
"weight": 396,
"weight": 390,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14361,7 +14363,7 @@
},
"x-appwrite": {
"method": "createSms",
"weight": 388,
"weight": 382,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14472,7 +14474,7 @@
},
"x-appwrite": {
"method": "updateSms",
"weight": 395,
"weight": 389,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14586,7 +14588,7 @@
},
"x-appwrite": {
"method": "getMessage",
"weight": 393,
"weight": 387,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14641,7 +14643,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 397,
"weight": 391,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14705,7 +14707,7 @@
},
"x-appwrite": {
"method": "listMessageLogs",
"weight": 391,
"weight": 385,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14782,7 +14784,7 @@
},
"x-appwrite": {
"method": "listTargets",
"weight": 392,
"weight": 386,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14859,7 +14861,7 @@
},
"x-appwrite": {
"method": "listProviders",
"weight": 362,
"weight": 356,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14937,7 +14939,7 @@
},
"x-appwrite": {
"method": "createApnsProvider",
"weight": 361,
"weight": 355,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15044,7 +15046,7 @@
},
"x-appwrite": {
"method": "updateApnsProvider",
"weight": 374,
"weight": 368,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15154,7 +15156,7 @@
},
"x-appwrite": {
"method": "createFcmProvider",
"weight": 360,
"weight": 354,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15241,7 +15243,7 @@
},
"x-appwrite": {
"method": "updateFcmProvider",
"weight": 373,
"weight": 367,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15331,7 +15333,7 @@
},
"x-appwrite": {
"method": "createMailgunProvider",
"weight": 352,
"weight": 346,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15448,7 +15450,7 @@
},
"x-appwrite": {
"method": "updateMailgunProvider",
"weight": 365,
"weight": 359,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15568,7 +15570,7 @@
},
"x-appwrite": {
"method": "createMsg91Provider",
"weight": 355,
"weight": 349,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15665,7 +15667,7 @@
},
"x-appwrite": {
"method": "updateMsg91Provider",
"weight": 368,
"weight": 362,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15765,7 +15767,7 @@
},
"x-appwrite": {
"method": "createSendgridProvider",
"weight": 353,
"weight": 347,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15872,7 +15874,7 @@
},
"x-appwrite": {
"method": "updateSendgridProvider",
"weight": 366,
"weight": 360,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15982,7 +15984,7 @@
},
"x-appwrite": {
"method": "createSmtpProvider",
"weight": 354,
"weight": 348,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16127,7 +16129,7 @@
},
"x-appwrite": {
"method": "updateSmtpProvider",
"weight": 367,
"weight": 361,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16274,7 +16276,7 @@
},
"x-appwrite": {
"method": "createTelesignProvider",
"weight": 356,
"weight": 350,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16371,7 +16373,7 @@
},
"x-appwrite": {
"method": "updateTelesignProvider",
"weight": 369,
"weight": 363,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16471,7 +16473,7 @@
},
"x-appwrite": {
"method": "createTextmagicProvider",
"weight": 357,
"weight": 351,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16568,7 +16570,7 @@
},
"x-appwrite": {
"method": "updateTextmagicProvider",
"weight": 370,
"weight": 364,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16668,7 +16670,7 @@
},
"x-appwrite": {
"method": "createTwilioProvider",
"weight": 358,
"weight": 352,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16765,7 +16767,7 @@
},
"x-appwrite": {
"method": "updateTwilioProvider",
"weight": 371,
"weight": 365,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16865,7 +16867,7 @@
},
"x-appwrite": {
"method": "createVonageProvider",
"weight": 359,
"weight": 353,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16962,7 +16964,7 @@
},
"x-appwrite": {
"method": "updateVonageProvider",
"weight": 372,
"weight": 366,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17062,7 +17064,7 @@
},
"x-appwrite": {
"method": "getProvider",
"weight": 364,
"weight": 358,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17117,7 +17119,7 @@
},
"x-appwrite": {
"method": "deleteProvider",
"weight": 375,
"weight": 369,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17181,7 +17183,7 @@
},
"x-appwrite": {
"method": "listProviderLogs",
"weight": 363,
"weight": 357,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17258,7 +17260,7 @@
},
"x-appwrite": {
"method": "listSubscriberLogs",
"weight": 384,
"weight": 378,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17335,7 +17337,7 @@
},
"x-appwrite": {
"method": "listTopics",
"weight": 377,
"weight": 371,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17411,7 +17413,7 @@
},
"x-appwrite": {
"method": "createTopic",
"weight": 376,
"weight": 370,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17496,7 +17498,7 @@
},
"x-appwrite": {
"method": "getTopic",
"weight": 379,
"weight": 373,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17558,7 +17560,7 @@
},
"x-appwrite": {
"method": "updateTopic",
"weight": 380,
"weight": 374,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17637,7 +17639,7 @@
},
"x-appwrite": {
"method": "deleteTopic",
"weight": 381,
"weight": 375,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17701,7 +17703,7 @@
},
"x-appwrite": {
"method": "listTopicLogs",
"weight": 378,
"weight": 372,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17778,7 +17780,7 @@
},
"x-appwrite": {
"method": "listSubscribers",
"weight": 383,
"weight": 377,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17864,7 +17866,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 382,
"weight": 376,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17956,7 +17958,7 @@
},
"x-appwrite": {
"method": "getSubscriber",
"weight": 385,
"weight": 379,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18021,7 +18023,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 386,
"weight": 380,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18098,7 +18100,7 @@
},
"x-appwrite": {
"method": "list",
"weight": 339,
"weight": 338,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18264,7 +18266,7 @@
},
"x-appwrite": {
"method": "getAppwriteReport",
"weight": 341,
"weight": 340,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18339,7 +18341,7 @@
},
"\/migrations\/firebase": {
"post": {
"summary": "Migrate Firebase data (Service Account)",
"summary": "Migrate Firebase data",
"operationId": "migrationsCreateFirebaseMigration",
"tags": [
"migrations"
@@ -18359,7 +18361,7 @@
},
"x-appwrite": {
"method": "createFirebaseMigration",
"weight": 336,
"weight": 335,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18415,177 +18417,6 @@
}
}
},
"\/migrations\/firebase\/deauthorize": {
"get": {
"summary": "Revoke Appwrite's authorization to access Firebase projects",
"operationId": "migrationsDeleteFirebaseAuth",
"tags": [
"migrations"
],
"description": "",
"responses": {
"200": {
"description": "File"
}
},
"x-appwrite": {
"method": "deleteFirebaseAuth",
"weight": 347,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "migrations\/delete-firebase-auth.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "migrations.write",
"platforms": [
"console"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
]
}
},
"\/migrations\/firebase\/oauth": {
"post": {
"summary": "Migrate Firebase data (OAuth)",
"operationId": "migrationsCreateFirebaseOAuthMigration",
"tags": [
"migrations"
],
"description": "",
"responses": {
"202": {
"description": "Migration",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/migration"
}
}
}
}
},
"x-appwrite": {
"method": "createFirebaseOAuthMigration",
"weight": 335,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "migrations\/create-firebase-o-auth-migration.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/migrations\/migration-firebase.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "migrations.write",
"platforms": [
"console"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
],
"requestBody": {
"content": {
"application\/json": {
"schema": {
"type": "object",
"properties": {
"resources": {
"type": "array",
"description": "List of resources to migrate",
"x-example": null,
"items": {
"type": "string"
}
},
"projectId": {
"type": "string",
"description": "Project ID of the Firebase Project",
"x-example": "<PROJECT_ID>"
}
},
"required": [
"resources",
"projectId"
]
}
}
}
}
}
},
"\/migrations\/firebase\/projects": {
"get": {
"summary": "List Firebase projects",
"operationId": "migrationsListFirebaseProjects",
"tags": [
"migrations"
],
"description": "",
"responses": {
"200": {
"description": "Migrations Firebase Projects List",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/firebaseProjectList"
}
}
}
}
},
"x-appwrite": {
"method": "listFirebaseProjects",
"weight": 346,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "migrations\/list-firebase-projects.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "migrations.read",
"platforms": [
"console"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
]
}
},
"\/migrations\/firebase\/report": {
"get": {
"summary": "Generate a report on Firebase data",
@@ -18608,7 +18439,7 @@
},
"x-appwrite": {
"method": "getFirebaseReport",
"weight": 342,
"weight": 341,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18660,80 +18491,6 @@
]
}
},
"\/migrations\/firebase\/report\/oauth": {
"get": {
"summary": "Generate a report on Firebase data using OAuth",
"operationId": "migrationsGetFirebaseReportOAuth",
"tags": [
"migrations"
],
"description": "",
"responses": {
"200": {
"description": "Migration Report",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/migrationReport"
}
}
}
}
},
"x-appwrite": {
"method": "getFirebaseReportOAuth",
"weight": 343,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "migrations\/get-firebase-report-o-auth.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/migrations\/migration-firebase-report.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "migrations.write",
"platforms": [
"console"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
],
"parameters": [
{
"name": "resources",
"description": "List of resources to migrate",
"required": true,
"schema": {
"type": "array",
"items": {
"type": "string"
}
},
"in": "query"
},
{
"name": "projectId",
"description": "Project ID",
"required": true,
"schema": {
"type": "string",
"x-example": "<PROJECT_ID>"
},
"in": "query"
}
]
}
},
"\/migrations\/nhost": {
"post": {
"summary": "Migrate NHost data",
@@ -18756,7 +18513,7 @@
},
"x-appwrite": {
"method": "createNHostMigration",
"weight": 338,
"weight": 337,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18869,7 +18626,7 @@
},
"x-appwrite": {
"method": "getNHostReport",
"weight": 349,
"weight": 343,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19004,7 +18761,7 @@
},
"x-appwrite": {
"method": "createSupabaseMigration",
"weight": 337,
"weight": 336,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19111,7 +18868,7 @@
},
"x-appwrite": {
"method": "getSupabaseReport",
"weight": 348,
"weight": 342,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19237,7 +18994,7 @@
},
"x-appwrite": {
"method": "get",
"weight": 340,
"weight": 339,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19297,7 +19054,7 @@
},
"x-appwrite": {
"method": "retry",
"weight": 350,
"weight": 344,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19350,7 +19107,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 351,
"weight": 345,
"cookies": false,
"type": "",
"deprecated": false,
@@ -20672,7 +20429,7 @@
},
"\/projects\/{projectId}\/auth\/memberships-privacy": {
"patch": {
"summary": "Update project team sensitive attributes",
"summary": "Update project memberships privacy attributes",
"operationId": "projectsUpdateMembershipsPrivacy",
"tags": [
"projects"
@@ -32749,30 +32506,6 @@
"migrations"
]
},
"firebaseProjectList": {
"description": "Migrations Firebase Projects List",
"type": "object",
"properties": {
"total": {
"type": "integer",
"description": "Total number of projects documents that matched your query.",
"x-example": 5,
"format": "int32"
},
"projects": {
"type": "array",
"description": "List of projects.",
"items": {
"$ref": "#\/components\/schemas\/firebaseProject"
},
"x-example": ""
}
},
"required": [
"total",
"projects"
]
},
"specificationList": {
"description": "Specifications List",
"type": "object",
@@ -35109,7 +34842,7 @@
},
"schedule": {
"type": "string",
"description": "Function execution schedult in CRON format.",
"description": "Function execution schedule in CRON format.",
"x-example": "5 4 * * *"
},
"timeout": {
@@ -36077,17 +35810,17 @@
"description": "Whether or not to send session alert emails to users.",
"x-example": true
},
"membershipsUserName": {
"authMembershipsUserName": {
"type": "boolean",
"description": "Whether or not to show user names in the teams membership response.",
"x-example": true
},
"membershipsUserEmail": {
"authMembershipsUserEmail": {
"type": "boolean",
"description": "Whether or not to show user emails in the teams membership response.",
"x-example": true
},
"membershipsMfa": {
"authMembershipsMfa": {
"type": "boolean",
"description": "Whether or not to show user MFA status in the teams membership response.",
"x-example": true
@@ -36297,9 +36030,9 @@
"authPersonalDataCheck",
"authMockNumbers",
"authSessionAlerts",
"membershipsUserName",
"membershipsUserEmail",
"membershipsMfa",
"authMembershipsUserName",
"authMembershipsUserEmail",
"authMembershipsMfa",
"oAuthProviders",
"platforms",
"webhooks",
@@ -38707,26 +38440,6 @@
"size",
"version"
]
},
"firebaseProject": {
"description": "MigrationFirebaseProject",
"type": "object",
"properties": {
"projectId": {
"type": "string",
"description": "Project ID.",
"x-example": "my-project"
},
"displayName": {
"type": "string",
"description": "Project display name.",
"x-example": "My Project"
}
},
"required": [
"projectId",
"displayName"
]
}
},
"securitySchemes": {
+53 -51
View File
@@ -1,7 +1,7 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2451,7 +2451,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -8596,7 +8596,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -9019,7 +9020,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -12527,7 +12529,7 @@
},
"x-appwrite": {
"method": "listMessages",
"weight": 390,
"weight": 384,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12606,7 +12608,7 @@
},
"x-appwrite": {
"method": "createEmail",
"weight": 387,
"weight": 381,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12753,7 +12755,7 @@
},
"x-appwrite": {
"method": "updateEmail",
"weight": 394,
"weight": 388,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12902,7 +12904,7 @@
},
"x-appwrite": {
"method": "createPush",
"weight": 389,
"weight": 383,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13060,7 +13062,7 @@
},
"x-appwrite": {
"method": "updatePush",
"weight": 396,
"weight": 390,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13220,7 +13222,7 @@
},
"x-appwrite": {
"method": "createSms",
"weight": 388,
"weight": 382,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13332,7 +13334,7 @@
},
"x-appwrite": {
"method": "updateSms",
"weight": 395,
"weight": 389,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13447,7 +13449,7 @@
},
"x-appwrite": {
"method": "getMessage",
"weight": 393,
"weight": 387,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13503,7 +13505,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 397,
"weight": 391,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13568,7 +13570,7 @@
},
"x-appwrite": {
"method": "listMessageLogs",
"weight": 391,
"weight": 385,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13646,7 +13648,7 @@
},
"x-appwrite": {
"method": "listTargets",
"weight": 392,
"weight": 386,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13724,7 +13726,7 @@
},
"x-appwrite": {
"method": "listProviders",
"weight": 362,
"weight": 356,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13803,7 +13805,7 @@
},
"x-appwrite": {
"method": "createApnsProvider",
"weight": 361,
"weight": 355,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13911,7 +13913,7 @@
},
"x-appwrite": {
"method": "updateApnsProvider",
"weight": 374,
"weight": 368,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14022,7 +14024,7 @@
},
"x-appwrite": {
"method": "createFcmProvider",
"weight": 360,
"weight": 354,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14110,7 +14112,7 @@
},
"x-appwrite": {
"method": "updateFcmProvider",
"weight": 373,
"weight": 367,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14201,7 +14203,7 @@
},
"x-appwrite": {
"method": "createMailgunProvider",
"weight": 352,
"weight": 346,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14319,7 +14321,7 @@
},
"x-appwrite": {
"method": "updateMailgunProvider",
"weight": 365,
"weight": 359,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14440,7 +14442,7 @@
},
"x-appwrite": {
"method": "createMsg91Provider",
"weight": 355,
"weight": 349,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14538,7 +14540,7 @@
},
"x-appwrite": {
"method": "updateMsg91Provider",
"weight": 368,
"weight": 362,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14639,7 +14641,7 @@
},
"x-appwrite": {
"method": "createSendgridProvider",
"weight": 353,
"weight": 347,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14747,7 +14749,7 @@
},
"x-appwrite": {
"method": "updateSendgridProvider",
"weight": 366,
"weight": 360,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14858,7 +14860,7 @@
},
"x-appwrite": {
"method": "createSmtpProvider",
"weight": 354,
"weight": 348,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15004,7 +15006,7 @@
},
"x-appwrite": {
"method": "updateSmtpProvider",
"weight": 367,
"weight": 361,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15152,7 +15154,7 @@
},
"x-appwrite": {
"method": "createTelesignProvider",
"weight": 356,
"weight": 350,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15250,7 +15252,7 @@
},
"x-appwrite": {
"method": "updateTelesignProvider",
"weight": 369,
"weight": 363,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15351,7 +15353,7 @@
},
"x-appwrite": {
"method": "createTextmagicProvider",
"weight": 357,
"weight": 351,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15449,7 +15451,7 @@
},
"x-appwrite": {
"method": "updateTextmagicProvider",
"weight": 370,
"weight": 364,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15550,7 +15552,7 @@
},
"x-appwrite": {
"method": "createTwilioProvider",
"weight": 358,
"weight": 352,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15648,7 +15650,7 @@
},
"x-appwrite": {
"method": "updateTwilioProvider",
"weight": 371,
"weight": 365,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15749,7 +15751,7 @@
},
"x-appwrite": {
"method": "createVonageProvider",
"weight": 359,
"weight": 353,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15847,7 +15849,7 @@
},
"x-appwrite": {
"method": "updateVonageProvider",
"weight": 372,
"weight": 366,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15948,7 +15950,7 @@
},
"x-appwrite": {
"method": "getProvider",
"weight": 364,
"weight": 358,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16004,7 +16006,7 @@
},
"x-appwrite": {
"method": "deleteProvider",
"weight": 375,
"weight": 369,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16069,7 +16071,7 @@
},
"x-appwrite": {
"method": "listProviderLogs",
"weight": 363,
"weight": 357,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16147,7 +16149,7 @@
},
"x-appwrite": {
"method": "listSubscriberLogs",
"weight": 384,
"weight": 378,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16225,7 +16227,7 @@
},
"x-appwrite": {
"method": "listTopics",
"weight": 377,
"weight": 371,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16302,7 +16304,7 @@
},
"x-appwrite": {
"method": "createTopic",
"weight": 376,
"weight": 370,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16388,7 +16390,7 @@
},
"x-appwrite": {
"method": "getTopic",
"weight": 379,
"weight": 373,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16451,7 +16453,7 @@
},
"x-appwrite": {
"method": "updateTopic",
"weight": 380,
"weight": 374,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16531,7 +16533,7 @@
},
"x-appwrite": {
"method": "deleteTopic",
"weight": 381,
"weight": 375,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16596,7 +16598,7 @@
},
"x-appwrite": {
"method": "listTopicLogs",
"weight": 378,
"weight": 372,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16674,7 +16676,7 @@
},
"x-appwrite": {
"method": "listSubscribers",
"weight": 383,
"weight": 377,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16761,7 +16763,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 382,
"weight": 376,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16855,7 +16857,7 @@
},
"x-appwrite": {
"method": "getSubscriber",
"weight": 385,
"weight": 379,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16921,7 +16923,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 386,
"weight": 380,
"cookies": false,
"type": "",
"deprecated": false,
@@ -25746,7 +25748,7 @@
},
"schedule": {
"type": "string",
"description": "Function execution schedult in CRON format.",
"description": "Function execution schedule in CRON format.",
"x-example": "5 4 * * *"
},
"timeout": {
+4 -4
View File
@@ -1,7 +1,7 @@
{
"swagger": "2.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2922,7 +2922,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -5981,7 +5981,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 382,
"weight": 376,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6070,7 +6070,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 386,
"weight": 380,
"cookies": false,
"type": "",
"deprecated": false,
+72 -374
View File
@@ -1,7 +1,7 @@
{
"swagger": "2.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2949,7 +2949,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -9610,7 +9610,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -10286,7 +10287,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -13890,7 +13892,7 @@
},
"x-appwrite": {
"method": "listMessages",
"weight": 390,
"weight": 384,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13967,7 +13969,7 @@
},
"x-appwrite": {
"method": "createEmail",
"weight": 387,
"weight": 381,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14127,7 +14129,7 @@
},
"x-appwrite": {
"method": "updateEmail",
"weight": 394,
"weight": 388,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14284,7 +14286,7 @@
},
"x-appwrite": {
"method": "createPush",
"weight": 389,
"weight": 383,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14459,7 +14461,7 @@
},
"x-appwrite": {
"method": "updatePush",
"weight": 396,
"weight": 390,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14631,7 +14633,7 @@
},
"x-appwrite": {
"method": "createSms",
"weight": 388,
"weight": 382,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14751,7 +14753,7 @@
},
"x-appwrite": {
"method": "updateSms",
"weight": 395,
"weight": 389,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14869,7 +14871,7 @@
},
"x-appwrite": {
"method": "getMessage",
"weight": 393,
"weight": 387,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14928,7 +14930,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 397,
"weight": 391,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14992,7 +14994,7 @@
},
"x-appwrite": {
"method": "listMessageLogs",
"weight": 391,
"weight": 385,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15068,7 +15070,7 @@
},
"x-appwrite": {
"method": "listTargets",
"weight": 392,
"weight": 386,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15144,7 +15146,7 @@
},
"x-appwrite": {
"method": "listProviders",
"weight": 362,
"weight": 356,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15221,7 +15223,7 @@
},
"x-appwrite": {
"method": "createApnsProvider",
"weight": 361,
"weight": 355,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15338,7 +15340,7 @@
},
"x-appwrite": {
"method": "updateApnsProvider",
"weight": 374,
"weight": 368,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15453,7 +15455,7 @@
},
"x-appwrite": {
"method": "createFcmProvider",
"weight": 360,
"weight": 354,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15546,7 +15548,7 @@
},
"x-appwrite": {
"method": "updateFcmProvider",
"weight": 373,
"weight": 367,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15637,7 +15639,7 @@
},
"x-appwrite": {
"method": "createMailgunProvider",
"weight": 352,
"weight": 346,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15766,7 +15768,7 @@
},
"x-appwrite": {
"method": "updateMailgunProvider",
"weight": 365,
"weight": 359,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15893,7 +15895,7 @@
},
"x-appwrite": {
"method": "createMsg91Provider",
"weight": 355,
"weight": 349,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15998,7 +16000,7 @@
},
"x-appwrite": {
"method": "updateMsg91Provider",
"weight": 368,
"weight": 362,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16101,7 +16103,7 @@
},
"x-appwrite": {
"method": "createSendgridProvider",
"weight": 353,
"weight": 347,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16218,7 +16220,7 @@
},
"x-appwrite": {
"method": "updateSendgridProvider",
"weight": 366,
"weight": 360,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16333,7 +16335,7 @@
},
"x-appwrite": {
"method": "createSmtpProvider",
"weight": 354,
"weight": 348,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16494,7 +16496,7 @@
},
"x-appwrite": {
"method": "updateSmtpProvider",
"weight": 367,
"weight": 361,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16652,7 +16654,7 @@
},
"x-appwrite": {
"method": "createTelesignProvider",
"weight": 356,
"weight": 350,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16757,7 +16759,7 @@
},
"x-appwrite": {
"method": "updateTelesignProvider",
"weight": 369,
"weight": 363,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16860,7 +16862,7 @@
},
"x-appwrite": {
"method": "createTextmagicProvider",
"weight": 357,
"weight": 351,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16965,7 +16967,7 @@
},
"x-appwrite": {
"method": "updateTextmagicProvider",
"weight": 370,
"weight": 364,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17068,7 +17070,7 @@
},
"x-appwrite": {
"method": "createTwilioProvider",
"weight": 358,
"weight": 352,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17173,7 +17175,7 @@
},
"x-appwrite": {
"method": "updateTwilioProvider",
"weight": 371,
"weight": 365,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17276,7 +17278,7 @@
},
"x-appwrite": {
"method": "createVonageProvider",
"weight": 359,
"weight": 353,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17381,7 +17383,7 @@
},
"x-appwrite": {
"method": "updateVonageProvider",
"weight": 372,
"weight": 366,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17484,7 +17486,7 @@
},
"x-appwrite": {
"method": "getProvider",
"weight": 364,
"weight": 358,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17543,7 +17545,7 @@
},
"x-appwrite": {
"method": "deleteProvider",
"weight": 375,
"weight": 369,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17607,7 +17609,7 @@
},
"x-appwrite": {
"method": "listProviderLogs",
"weight": 363,
"weight": 357,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17683,7 +17685,7 @@
},
"x-appwrite": {
"method": "listSubscriberLogs",
"weight": 384,
"weight": 378,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17759,7 +17761,7 @@
},
"x-appwrite": {
"method": "listTopics",
"weight": 377,
"weight": 371,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17834,7 +17836,7 @@
},
"x-appwrite": {
"method": "createTopic",
"weight": 376,
"weight": 370,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17926,7 +17928,7 @@
},
"x-appwrite": {
"method": "getTopic",
"weight": 379,
"weight": 373,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17988,7 +17990,7 @@
},
"x-appwrite": {
"method": "updateTopic",
"weight": 380,
"weight": 374,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18071,7 +18073,7 @@
},
"x-appwrite": {
"method": "deleteTopic",
"weight": 381,
"weight": 375,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18135,7 +18137,7 @@
},
"x-appwrite": {
"method": "listTopicLogs",
"weight": 378,
"weight": 372,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18211,7 +18213,7 @@
},
"x-appwrite": {
"method": "listSubscribers",
"weight": 383,
"weight": 377,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18294,7 +18296,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 382,
"weight": 376,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18386,7 +18388,7 @@
},
"x-appwrite": {
"method": "getSubscriber",
"weight": 385,
"weight": 379,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18453,7 +18455,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 386,
"weight": 380,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18528,7 +18530,7 @@
},
"x-appwrite": {
"method": "list",
"weight": 339,
"weight": 338,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18699,7 +18701,7 @@
},
"x-appwrite": {
"method": "getAppwriteReport",
"weight": 341,
"weight": 340,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18767,7 +18769,7 @@
},
"\/migrations\/firebase": {
"post": {
"summary": "Migrate Firebase data (Service Account)",
"summary": "Migrate Firebase data",
"operationId": "migrationsCreateFirebaseMigration",
"consumes": [
"application\/json"
@@ -18789,7 +18791,7 @@
},
"x-appwrite": {
"method": "createFirebaseMigration",
"weight": 336,
"weight": 335,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18847,192 +18849,6 @@
]
}
},
"\/migrations\/firebase\/deauthorize": {
"get": {
"summary": "Revoke Appwrite's authorization to access Firebase projects",
"operationId": "migrationsDeleteFirebaseAuth",
"consumes": [
"application\/json"
],
"produces": [
"application\/json"
],
"tags": [
"migrations"
],
"description": "",
"responses": {
"200": {
"description": "File",
"schema": {
"type": "file"
}
}
},
"x-appwrite": {
"method": "deleteFirebaseAuth",
"weight": 347,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "migrations\/delete-firebase-auth.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "migrations.write",
"platforms": [
"console"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
]
}
},
"\/migrations\/firebase\/oauth": {
"post": {
"summary": "Migrate Firebase data (OAuth)",
"operationId": "migrationsCreateFirebaseOAuthMigration",
"consumes": [
"application\/json"
],
"produces": [
"application\/json"
],
"tags": [
"migrations"
],
"description": "",
"responses": {
"202": {
"description": "Migration",
"schema": {
"$ref": "#\/definitions\/migration"
}
}
},
"x-appwrite": {
"method": "createFirebaseOAuthMigration",
"weight": 335,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "migrations\/create-firebase-o-auth-migration.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/migrations\/migration-firebase.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "migrations.write",
"platforms": [
"console"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
],
"parameters": [
{
"name": "payload",
"in": "body",
"schema": {
"type": "object",
"properties": {
"resources": {
"type": "array",
"description": "List of resources to migrate",
"default": null,
"x-example": null,
"items": {
"type": "string"
}
},
"projectId": {
"type": "string",
"description": "Project ID of the Firebase Project",
"default": null,
"x-example": "<PROJECT_ID>"
}
},
"required": [
"resources",
"projectId"
]
}
}
]
}
},
"\/migrations\/firebase\/projects": {
"get": {
"summary": "List Firebase projects",
"operationId": "migrationsListFirebaseProjects",
"consumes": [
"application\/json"
],
"produces": [
"application\/json"
],
"tags": [
"migrations"
],
"description": "",
"responses": {
"200": {
"description": "Migrations Firebase Projects List",
"schema": {
"$ref": "#\/definitions\/firebaseProjectList"
}
}
},
"x-appwrite": {
"method": "listFirebaseProjects",
"weight": 346,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "migrations\/list-firebase-projects.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "migrations.read",
"platforms": [
"console"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
]
}
},
"\/migrations\/firebase\/report": {
"get": {
"summary": "Generate a report on Firebase data",
@@ -19057,7 +18873,7 @@
},
"x-appwrite": {
"method": "getFirebaseReport",
"weight": 342,
"weight": 341,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19106,79 +18922,6 @@
]
}
},
"\/migrations\/firebase\/report\/oauth": {
"get": {
"summary": "Generate a report on Firebase data using OAuth",
"operationId": "migrationsGetFirebaseReportOAuth",
"consumes": [
"application\/json"
],
"produces": [
"application\/json"
],
"tags": [
"migrations"
],
"description": "",
"responses": {
"200": {
"description": "Migration Report",
"schema": {
"$ref": "#\/definitions\/migrationReport"
}
}
},
"x-appwrite": {
"method": "getFirebaseReportOAuth",
"weight": 343,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "migrations\/get-firebase-report-o-auth.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/migrations\/migration-firebase-report.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "migrations.write",
"platforms": [
"console"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
],
"parameters": [
{
"name": "resources",
"description": "List of resources to migrate",
"required": true,
"type": "array",
"collectionFormat": "multi",
"items": {
"type": "string"
},
"in": "query"
},
{
"name": "projectId",
"description": "Project ID",
"required": true,
"type": "string",
"x-example": "<PROJECT_ID>",
"in": "query"
}
]
}
},
"\/migrations\/nhost": {
"post": {
"summary": "Migrate NHost data",
@@ -19203,7 +18946,7 @@
},
"x-appwrite": {
"method": "createNHostMigration",
"weight": 338,
"weight": 337,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19326,7 +19069,7 @@
},
"x-appwrite": {
"method": "getNHostReport",
"weight": 349,
"weight": 343,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19448,7 +19191,7 @@
},
"x-appwrite": {
"method": "createSupabaseMigration",
"weight": 337,
"weight": 336,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19564,7 +19307,7 @@
},
"x-appwrite": {
"method": "getSupabaseReport",
"weight": 348,
"weight": 342,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19679,7 +19422,7 @@
},
"x-appwrite": {
"method": "get",
"weight": 340,
"weight": 339,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19739,7 +19482,7 @@
},
"x-appwrite": {
"method": "retry",
"weight": 350,
"weight": 344,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19794,7 +19537,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 351,
"weight": 345,
"cookies": false,
"type": "",
"deprecated": false,
@@ -21138,7 +20881,7 @@
},
"\/projects\/{projectId}\/auth\/memberships-privacy": {
"patch": {
"summary": "Update project team sensitive attributes",
"summary": "Update project memberships privacy attributes",
"operationId": "projectsUpdateMembershipsPrivacy",
"consumes": [
"application\/json"
@@ -33250,31 +32993,6 @@
"migrations"
]
},
"firebaseProjectList": {
"description": "Migrations Firebase Projects List",
"type": "object",
"properties": {
"total": {
"type": "integer",
"description": "Total number of projects documents that matched your query.",
"x-example": 5,
"format": "int32"
},
"projects": {
"type": "array",
"description": "List of projects.",
"items": {
"type": "object",
"$ref": "#\/definitions\/firebaseProject"
},
"x-example": ""
}
},
"required": [
"total",
"projects"
]
},
"specificationList": {
"description": "Specifications List",
"type": "object",
@@ -35618,7 +35336,7 @@
},
"schedule": {
"type": "string",
"description": "Function execution schedult in CRON format.",
"description": "Function execution schedule in CRON format.",
"x-example": "5 4 * * *"
},
"timeout": {
@@ -36591,17 +36309,17 @@
"description": "Whether or not to send session alert emails to users.",
"x-example": true
},
"membershipsUserName": {
"authMembershipsUserName": {
"type": "boolean",
"description": "Whether or not to show user names in the teams membership response.",
"x-example": true
},
"membershipsUserEmail": {
"authMembershipsUserEmail": {
"type": "boolean",
"description": "Whether or not to show user emails in the teams membership response.",
"x-example": true
},
"membershipsMfa": {
"authMembershipsMfa": {
"type": "boolean",
"description": "Whether or not to show user MFA status in the teams membership response.",
"x-example": true
@@ -36815,9 +36533,9 @@
"authPersonalDataCheck",
"authMockNumbers",
"authSessionAlerts",
"membershipsUserName",
"membershipsUserEmail",
"membershipsMfa",
"authMembershipsUserName",
"authMembershipsUserEmail",
"authMembershipsMfa",
"oAuthProviders",
"platforms",
"webhooks",
@@ -39274,26 +38992,6 @@
"size",
"version"
]
},
"firebaseProject": {
"description": "MigrationFirebaseProject",
"type": "object",
"properties": {
"projectId": {
"type": "string",
"description": "Project ID.",
"x-example": "my-project"
},
"displayName": {
"type": "string",
"description": "Project display name.",
"x-example": "My Project"
}
},
"required": [
"projectId",
"displayName"
]
}
},
"externalDocs": {
+53 -51
View File
@@ -1,7 +1,7 @@
{
"swagger": "2.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2604,7 +2604,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -8720,7 +8720,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -9166,7 +9167,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -12752,7 +12754,7 @@
},
"x-appwrite": {
"method": "listMessages",
"weight": 390,
"weight": 384,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12830,7 +12832,7 @@
},
"x-appwrite": {
"method": "createEmail",
"weight": 387,
"weight": 381,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12991,7 +12993,7 @@
},
"x-appwrite": {
"method": "updateEmail",
"weight": 394,
"weight": 388,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13149,7 +13151,7 @@
},
"x-appwrite": {
"method": "createPush",
"weight": 389,
"weight": 383,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13325,7 +13327,7 @@
},
"x-appwrite": {
"method": "updatePush",
"weight": 396,
"weight": 390,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13498,7 +13500,7 @@
},
"x-appwrite": {
"method": "createSms",
"weight": 388,
"weight": 382,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13619,7 +13621,7 @@
},
"x-appwrite": {
"method": "updateSms",
"weight": 395,
"weight": 389,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13738,7 +13740,7 @@
},
"x-appwrite": {
"method": "getMessage",
"weight": 393,
"weight": 387,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13798,7 +13800,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 397,
"weight": 391,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13863,7 +13865,7 @@
},
"x-appwrite": {
"method": "listMessageLogs",
"weight": 391,
"weight": 385,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13940,7 +13942,7 @@
},
"x-appwrite": {
"method": "listTargets",
"weight": 392,
"weight": 386,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14017,7 +14019,7 @@
},
"x-appwrite": {
"method": "listProviders",
"weight": 362,
"weight": 356,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14095,7 +14097,7 @@
},
"x-appwrite": {
"method": "createApnsProvider",
"weight": 361,
"weight": 355,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14213,7 +14215,7 @@
},
"x-appwrite": {
"method": "updateApnsProvider",
"weight": 374,
"weight": 368,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14329,7 +14331,7 @@
},
"x-appwrite": {
"method": "createFcmProvider",
"weight": 360,
"weight": 354,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14423,7 +14425,7 @@
},
"x-appwrite": {
"method": "updateFcmProvider",
"weight": 373,
"weight": 367,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14515,7 +14517,7 @@
},
"x-appwrite": {
"method": "createMailgunProvider",
"weight": 352,
"weight": 346,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14645,7 +14647,7 @@
},
"x-appwrite": {
"method": "updateMailgunProvider",
"weight": 365,
"weight": 359,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14773,7 +14775,7 @@
},
"x-appwrite": {
"method": "createMsg91Provider",
"weight": 355,
"weight": 349,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14879,7 +14881,7 @@
},
"x-appwrite": {
"method": "updateMsg91Provider",
"weight": 368,
"weight": 362,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14983,7 +14985,7 @@
},
"x-appwrite": {
"method": "createSendgridProvider",
"weight": 353,
"weight": 347,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15101,7 +15103,7 @@
},
"x-appwrite": {
"method": "updateSendgridProvider",
"weight": 366,
"weight": 360,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15217,7 +15219,7 @@
},
"x-appwrite": {
"method": "createSmtpProvider",
"weight": 354,
"weight": 348,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15379,7 +15381,7 @@
},
"x-appwrite": {
"method": "updateSmtpProvider",
"weight": 367,
"weight": 361,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15538,7 +15540,7 @@
},
"x-appwrite": {
"method": "createTelesignProvider",
"weight": 356,
"weight": 350,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15644,7 +15646,7 @@
},
"x-appwrite": {
"method": "updateTelesignProvider",
"weight": 369,
"weight": 363,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15748,7 +15750,7 @@
},
"x-appwrite": {
"method": "createTextmagicProvider",
"weight": 357,
"weight": 351,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15854,7 +15856,7 @@
},
"x-appwrite": {
"method": "updateTextmagicProvider",
"weight": 370,
"weight": 364,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15958,7 +15960,7 @@
},
"x-appwrite": {
"method": "createTwilioProvider",
"weight": 358,
"weight": 352,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16064,7 +16066,7 @@
},
"x-appwrite": {
"method": "updateTwilioProvider",
"weight": 371,
"weight": 365,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16168,7 +16170,7 @@
},
"x-appwrite": {
"method": "createVonageProvider",
"weight": 359,
"weight": 353,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16274,7 +16276,7 @@
},
"x-appwrite": {
"method": "updateVonageProvider",
"weight": 372,
"weight": 366,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16378,7 +16380,7 @@
},
"x-appwrite": {
"method": "getProvider",
"weight": 364,
"weight": 358,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16438,7 +16440,7 @@
},
"x-appwrite": {
"method": "deleteProvider",
"weight": 375,
"weight": 369,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16503,7 +16505,7 @@
},
"x-appwrite": {
"method": "listProviderLogs",
"weight": 363,
"weight": 357,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16580,7 +16582,7 @@
},
"x-appwrite": {
"method": "listSubscriberLogs",
"weight": 384,
"weight": 378,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16657,7 +16659,7 @@
},
"x-appwrite": {
"method": "listTopics",
"weight": 377,
"weight": 371,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16733,7 +16735,7 @@
},
"x-appwrite": {
"method": "createTopic",
"weight": 376,
"weight": 370,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16826,7 +16828,7 @@
},
"x-appwrite": {
"method": "getTopic",
"weight": 379,
"weight": 373,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16889,7 +16891,7 @@
},
"x-appwrite": {
"method": "updateTopic",
"weight": 380,
"weight": 374,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16973,7 +16975,7 @@
},
"x-appwrite": {
"method": "deleteTopic",
"weight": 381,
"weight": 375,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17038,7 +17040,7 @@
},
"x-appwrite": {
"method": "listTopicLogs",
"weight": 378,
"weight": 372,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17115,7 +17117,7 @@
},
"x-appwrite": {
"method": "listSubscribers",
"weight": 383,
"weight": 377,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17199,7 +17201,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 382,
"weight": 376,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17293,7 +17295,7 @@
},
"x-appwrite": {
"method": "getSubscriber",
"weight": 385,
"weight": 379,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17361,7 +17363,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 386,
"weight": 380,
"cookies": false,
"type": "",
"deprecated": false,
@@ -26233,7 +26235,7 @@
},
"schedule": {
"type": "string",
"description": "Function execution schedult in CRON format.",
"description": "Function execution schedule in CRON format.",
"x-example": "5 4 * * *"
},
"timeout": {
+15 -15
View File
@@ -61,9 +61,9 @@ $avatarCallback = function (string $type, string $code, int $width, int $height,
unset($image);
};
$getUserGitHub = function (string $userId, Document $project, Database $dbForProject, Database $dbForConsole, ?Logger $logger) {
$getUserGitHub = function (string $userId, Document $project, Database $dbForProject, Database $dbForPlatform, ?Logger $logger) {
try {
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
$sessions = $user->getAttribute('sessions', []);
@@ -122,7 +122,7 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro
do {
$previousAccessToken = $gitHubSession->getAttribute('providerAccessToken');
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
$sessions = $user->getAttribute('sessions', []);
$gitHubSession = new Document();
@@ -565,14 +565,14 @@ App::get('/v1/cards/cloud')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('response')
->inject('heroes')
->inject('contributors')
->inject('employees')
->inject('logger')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
if ($user->isEmpty() && empty($mock)) {
throw new Exception(Exception::USER_NOT_FOUND);
@@ -583,7 +583,7 @@ App::get('/v1/cards/cloud')
$email = $user->getAttribute('email', '');
$createdAt = new \DateTime($user->getCreatedAt());
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger);
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger);
$githubName = $gitHub['name'] ?? '';
$githubId = $gitHub['id'] ?? '';
@@ -772,14 +772,14 @@ App::get('/v1/cards/cloud-back')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('response')
->inject('heroes')
->inject('contributors')
->inject('employees')
->inject('logger')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
if ($user->isEmpty() && empty($mock)) {
throw new Exception(Exception::USER_NOT_FOUND);
@@ -789,7 +789,7 @@ App::get('/v1/cards/cloud-back')
$userId = $user->getId();
$email = $user->getAttribute('email', '');
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger);
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger);
$githubId = $gitHub['id'] ?? '';
$isHero = \array_key_exists($email, $heroes);
@@ -850,14 +850,14 @@ App::get('/v1/cards/cloud-og')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('response')
->inject('heroes')
->inject('contributors')
->inject('employees')
->inject('logger')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
if ($user->isEmpty() && empty($mock)) {
throw new Exception(Exception::USER_NOT_FOUND);
@@ -872,7 +872,7 @@ App::get('/v1/cards/cloud-og')
$email = $user->getAttribute('email', '');
$createdAt = new \DateTime($user->getCreatedAt());
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger);
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger);
$githubName = $gitHub['name'] ?? '';
$githubId = $gitHub['id'] ?? '';
+1 -1
View File
@@ -71,7 +71,7 @@ App::post('/v1/console/assistant')
->param('prompt', '', new Text(2000), 'Prompt. A string containing questions asked to the AI assistant.')
->inject('response')
->action(function (string $prompt, Response $response) {
$ch = curl_init('http://appwrite-assistant:3003/');
$ch = curl_init('http://appwrite-assistant:3003/v1/models/assistant/prompt');
$responseHeaders = [];
$query = json_encode(['prompt' => $prompt]);
$headers = ['accept: text/event-stream'];
+100 -40
View File
@@ -20,7 +20,6 @@ use Utopia\Audit\Audit;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception as DatabaseException;
use Utopia\Database\Exception\Authorization as AuthorizationException;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Duplicate as DuplicateException;
@@ -153,7 +152,7 @@ function createAttribute(string $databaseId, string $collectionId, Document $att
} catch (DuplicateException) {
throw new Exception(Exception::ATTRIBUTE_ALREADY_EXISTS);
} catch (LimitException) {
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, 'Attribute limit exceeded');
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED);
} catch (\Throwable $e) {
$dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId);
$dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $collection->getInternalId());
@@ -197,7 +196,7 @@ function createAttribute(string $databaseId, string $collectionId, Document $att
throw new Exception(Exception::ATTRIBUTE_ALREADY_EXISTS);
} catch (LimitException) {
$dbForProject->deleteDocument('attributes', $attribute->getId());
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, 'Attribute limit exceeded');
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED);
} catch (\Throwable $e) {
$dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $relatedCollection->getId());
$dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedCollection->getInternalId());
@@ -291,15 +290,9 @@ function updateAttribute(
$attribute->setAttribute('size', $size);
}
$formatOptions = $attribute->getAttribute('formatOptions');
switch ($attribute->getAttribute('format')) {
case APP_DATABASE_ATTRIBUTE_INT_RANGE:
case APP_DATABASE_ATTRIBUTE_FLOAT_RANGE:
if ($min === $formatOptions['min'] && $max === $formatOptions['max']) {
break;
}
if ($min > $max) {
throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Minimum value must be lesser than maximum value');
}
@@ -386,30 +379,42 @@ function updateAttribute(
size: $size,
required: $required,
default: $default,
formatOptions: $options ?? null,
formatOptions: $options,
newKey: $newKey ?? null
);
} catch (TruncateException) {
throw new Exception(Exception::ATTRIBUTE_INVALID_RESIZE);
} catch (NotFoundException) {
throw new Exception(Exception::ATTRIBUTE_NOT_FOUND);
} catch (LimitException) {
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED);
}
}
if (!empty($newKey) && $key !== $newKey) {
// Delete attribute and recreate since we can't modify IDs
$original = clone $attribute;
$dbForProject->deleteDocument('attributes', $attribute->getId());
$originalUid = $attribute->getId();
$attribute
->setAttribute('$id', ID::custom($db->getInternalId() . '_' . $collection->getInternalId() . '_' . $newKey))
->setAttribute('key', $newKey);
try {
$attribute = $dbForProject->createDocument('attributes', $attribute);
} catch (DatabaseException|PDOException) {
$attribute = $dbForProject->createDocument('attributes', $original);
$dbForProject->updateDocument('attributes', $originalUid, $attribute);
/**
* @var Document $index
*/
foreach ($collection->getAttribute('indexes') as $index) {
/**
* @var string[] $attributes
*/
$attributes = $index->getAttribute('attributes', []);
$found = \array_search($key, $attributes);
if ($found !== false) {
$attributes[$found] = $newKey;
$index->setAttribute('attributes', $attributes);
$dbForProject->updateDocument('indexes', $index->getId(), $index);
}
}
} else {
$attribute = $dbForProject->updateDocument('attributes', $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key, $attribute);
@@ -2587,7 +2592,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
$attributeStatus = $oldAttributes[$attributeIndex]['status'];
$attributeType = $oldAttributes[$attributeIndex]['type'];
$attributeSize = $oldAttributes[$attributeIndex]['size'];
$attributeArray = $oldAttributes[$attributeIndex]['array'] ?? false;
if ($attributeType === Database::VAR_RELATIONSHIP) {
@@ -2601,10 +2605,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
$lengths[$i] = null;
if ($attributeType === Database::VAR_STRING) {
$lengths[$i] = $attributeSize; // set attribute size as index length only for strings
}
if ($attributeArray === true) {
$lengths[$i] = Database::ARRAY_INDEX_LENGTH;
$orders[$i] = null;
@@ -2627,7 +2627,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
$validator = new IndexValidator(
$collection->getAttribute('attributes'),
$dbForProject->getAdapter()->getMaxIndexLength()
$dbForProject->getAdapter()->getMaxIndexLength(),
$dbForProject->getAdapter()->getInternalIndexesKeys(),
);
if (!$validator->isValid($index)) {
throw new Exception(Exception::INDEX_INVALID, $validator->getDescription());
@@ -2943,7 +2944,11 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
$data['$permissions'] = $permissions;
$document = new Document($data);
$checkPermissions = function (Document $collection, Document $document, string $permission) use (&$checkPermissions, $dbForProject, $database) {
$operations = 0;
$checkPermissions = function (Document $collection, Document $document, string $permission) use (&$checkPermissions, $dbForProject, $database, &$operations) {
$operations++;
$documentSecurity = $collection->getAttribute('documentSecurity', false);
$validator = new Authorization($permission);
@@ -3035,6 +3040,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
// Add $collectionId and $databaseId for all documents
$processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) {
$document->setAttribute('$databaseId', $database->getId());
@@ -3070,6 +3076,13 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
$processDocument($collection, $document);
$queueForUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, $operations)
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations)
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getInternalId(), $collection->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_STORAGE), 1); // per collection
$response->addHeader('X-Debug-Operations', $operations);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($document, Response::MODEL_DOCUMENT);
@@ -3089,10 +3102,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
->setContext('collection', $collection)
->setContext('database', $database)
->setPayload($response->getPayload(), sensitive: $relationships);
$queueForUsage
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getInternalId(), $collection->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_STORAGE), 1); // per collection
});
App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
@@ -3115,7 +3124,8 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
->inject('response')
->inject('dbForProject')
->inject('mode')
->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, string $mode) {
->inject('queueForUsage')
->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, string $mode, Usage $queueForUsage) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
@@ -3151,7 +3161,6 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$documentId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId));
@@ -3166,12 +3175,16 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
$documents = $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries);
$total = $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries, APP_LIMIT_COUNT);
$operations = 0;
// Add $collectionId and $databaseId for all documents
$processDocument = (function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database): bool {
$processDocument = (function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, &$operations): bool {
if ($document->isEmpty()) {
return false;
}
$operations++;
$document->removeAttribute('$collection');
$document->setAttribute('$databaseId', $database->getId());
$document->setAttribute('$collectionId', $collection->getId());
@@ -3185,8 +3198,13 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
$related = $document->getAttribute($relationship->getAttribute('key'));
if (empty($related)) {
if (\in_array(\gettype($related), ['array', 'object'])) {
$operations++;
}
continue;
}
if (!\is_array($related)) {
$relations = [$related];
} else {
@@ -3194,6 +3212,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
}
$relatedCollectionId = $relationship->getAttribute('relatedCollection');
// todo: Use local cache for this getDocument
$relatedCollection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId));
foreach ($relations as $index => $doc) {
@@ -3218,6 +3237,13 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
$processDocument($collection, $document);
}
$queueForUsage
->addMetric(METRIC_DATABASES_OPERATIONS_READS, $operations)
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations)
;
$response->addHeader('X-Debug-Operations', $operations);
$select = \array_reduce($queries, function ($result, $query) {
return $result || ($query->getMethod() === Query::TYPE_SELECT);
}, false);
@@ -3273,9 +3299,9 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
->inject('response')
->inject('dbForProject')
->inject('mode')
->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, string $mode) {
->inject('queueForUsage')
->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, string $mode, Usage $queueForUsage) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
@@ -3302,12 +3328,16 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
throw new Exception(Exception::DOCUMENT_NOT_FOUND);
}
$operations = 0;
// Add $collectionId and $databaseId for all documents
$processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) {
$processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, &$operations) {
if ($document->isEmpty()) {
return;
}
$operations++;
$document->setAttribute('$databaseId', $database->getId());
$document->setAttribute('$collectionId', $collection->getId());
@@ -3320,8 +3350,13 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
$related = $document->getAttribute($relationship->getAttribute('key'));
if (empty($related)) {
if (\in_array(\gettype($related), ['array', 'object'])) {
$operations++;
}
continue;
}
if (!\is_array($related)) {
$related = [$related];
}
@@ -3341,6 +3376,13 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
$processDocument($collection, $document);
$queueForUsage
->addMetric(METRIC_DATABASES_OPERATIONS_READS, $operations)
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations)
;
$response->addHeader('X-Debug-Operations', $operations);
$response->dynamic($document, Response::MODEL_DOCUMENT);
});
@@ -3443,6 +3485,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
$output[$i]['countryName'] = $locale->getText('locale.country.unknown');
}
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource),
'logs' => $output,
@@ -3480,7 +3523,8 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
->inject('dbForProject')
->inject('queueForEvents')
->inject('mode')
->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, string $mode) {
->inject('queueForUsage')
->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Usage $queueForUsage) {
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
@@ -3547,7 +3591,12 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
$data['$permissions'] = $permissions;
$newDocument = new Document($data);
$setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database) {
$operations = 0;
$setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database, &$operations) {
$operations++;
$relationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
@@ -3615,6 +3664,13 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
$setCollection($collection, $newDocument);
$queueForUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, $operations)
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations)
;
$response->addHeader('X-Debug-Operations', $operations);
try {
$document = $dbForProject->withRequestTimestamp(
$requestTimestamp,
@@ -3786,6 +3842,13 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
$processDocument($collection, $document);
$queueForUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, 1)
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_WRITES), 1)
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getInternalId(), $collection->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_STORAGE), 1); // per collection
$response->addHeader('X-Debug-Operations', 1);
$relationships = \array_map(
fn ($document) => $document->getAttribute('key'),
\array_filter(
@@ -3802,9 +3865,6 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
->setContext('database', $database)
->setPayload($response->output($document, Response::MODEL_DOCUMENT), sensitive: $relationships);
$queueForUsage
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getInternalId(), $collection->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_STORAGE), 1); // per collection
$response->noContent();
});
+74 -42
View File
@@ -23,6 +23,7 @@ use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Rule;
use Executor\Executor;
use MaxMind\Db\Reader;
use Utopia\Abuse\Abuse;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Config;
@@ -178,15 +179,45 @@ App::post('/v1/functions')
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('timelimit')
->inject('project')
->inject('user')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('gitHub')
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) {
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, callable $timelimit, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForPlatform, GitHub $github) use ($redeployVcs) {
$functionId = ($functionId == 'unique()') ? ID::unique() : $functionId;
// Temporary abuse check
$abuseCheck = function () use ($project, $timelimit, $response) {
$abuseKey = "projectId:{projectId},url:{url}";
$abuseLimit = App::getEnv('_APP_FUNCTIONS_CREATION_ABUSE_LIMIT', 50);
$abuseTime = 86400; // 1 day
$timeLimit = $timelimit($abuseKey, $abuseLimit, $abuseTime);
$timeLimit
->setParam('{projectId}', $project->getId())
->setParam('{url}', '/v1/functions');
$abuse = new Abuse($timeLimit);
$remaining = $timeLimit->remaining();
$limit = $timeLimit->limit();
$time = $timeLimit->time() + $abuseTime;
$response
->addHeader('X-RateLimit-Limit', $limit)
->addHeader('X-RateLimit-Remaining', $remaining)
->addHeader('X-RateLimit-Reset', $time);
$enabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled';
if ($enabled && $abuse->check()) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED);
}
};
$abuseCheck();
$allowList = \array_filter(\explode(',', System::getEnv('_APP_FUNCTIONS_RUNTIMES', '')));
if (!empty($allowList) && !\in_array($runtime, $allowList)) {
@@ -207,7 +238,7 @@ App::post('/v1/functions')
->setAttribute('version', $templateVersion);
}
$installation = $dbForConsole->getDocument('installations', $installationId);
$installation = $dbForPlatform->getDocument('installations', $installationId);
if (!empty($installationId) && $installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -249,7 +280,7 @@ App::post('/v1/functions')
]));
$schedule = Authorization::skip(
fn () => $dbForConsole->createDocument('schedules', new Document([
fn () => $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'), // Todo replace with projects region
'resourceType' => 'function',
'resourceId' => $function->getId(),
@@ -268,7 +299,7 @@ App::post('/v1/functions')
if (!empty($providerRepositoryId)) {
$teamId = $project->getAttribute('teamId', '');
$repository = $dbForConsole->createDocument('repositories', new Document([
$repository = $dbForPlatform->createDocument('repositories', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
@@ -328,10 +359,11 @@ App::post('/v1/functions')
if (!empty($functionsDomain)) {
$routeSubdomain = ID::unique();
$domain = "{$routeSubdomain}.{$functionsDomain}";
$ruleId = md5($domain);
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique();
$rule = Authorization::skip(
fn () => $dbForConsole->createDocument('rules', new Document([
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
@@ -803,9 +835,9 @@ App::put('/v1/functions/:functionId')
->inject('project')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('gitHub')
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) {
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForPlatform, GitHub $github) use ($redeployVcs) {
// TODO: If only branch changes, re-deploy
$function = $dbForProject->getDocument('functions', $functionId);
@@ -813,7 +845,7 @@ App::put('/v1/functions/:functionId')
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$installation = $dbForConsole->getDocument('installations', $installationId);
$installation = $dbForPlatform->getDocument('installations', $installationId);
if (!empty($installationId) && $installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -844,7 +876,7 @@ App::put('/v1/functions/:functionId')
// Git disconnect logic. Disconnecting only when providerRepositoryId is empty, allowing for continue updates without disconnecting git
if ($isConnected && ($providerRepositoryId !== null && empty($providerRepositoryId))) {
$repositories = $dbForConsole->find('repositories', [
$repositories = $dbForPlatform->find('repositories', [
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::equal('resourceInternalId', [$function->getInternalId()]),
Query::equal('resourceType', ['function']),
@@ -852,7 +884,7 @@ App::put('/v1/functions/:functionId')
]);
foreach ($repositories as $repository) {
$dbForConsole->deleteDocument('repositories', $repository->getId());
$dbForPlatform->deleteDocument('repositories', $repository->getId());
}
$providerRepositoryId = '';
@@ -868,7 +900,7 @@ App::put('/v1/functions/:functionId')
if (!$isConnected && !empty($providerRepositoryId)) {
$teamId = $project->getAttribute('teamId', '');
$repository = $dbForConsole->createDocument('repositories', new Document([
$repository = $dbForPlatform->createDocument('repositories', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
@@ -950,12 +982,12 @@ App::put('/v1/functions/:functionId')
}
// Inform scheduler if function is still active
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$queueForEvents->setParam('functionId', $function->getId());
@@ -1068,8 +1100,8 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('dbForConsole')
->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForPlatform) {
$function = $dbForProject->getDocument('functions', $functionId);
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
@@ -1097,12 +1129,12 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId')
])));
// Inform scheduler if function is still active
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$queueForEvents
->setParam('functionId', $function->getId())
@@ -1130,8 +1162,8 @@ App::delete('/v1/functions/:functionId')
->inject('dbForProject')
->inject('queueForDeletes')
->inject('queueForEvents')
->inject('dbForConsole')
->action(function (string $functionId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $functionId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Database $dbForPlatform) {
$function = $dbForProject->getDocument('functions', $functionId);
@@ -1144,11 +1176,11 @@ App::delete('/v1/functions/:functionId')
}
// Inform scheduler to no longer run function
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('active', false);
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
@@ -1758,13 +1790,13 @@ App::post('/v1/functions/:functionId/executions')
->inject('request')
->inject('project')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('user')
->inject('queueForEvents')
->inject('queueForUsage')
->inject('queueForFunctions')
->inject('geodb')
->action(function (string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForConsole, Document $user, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb) {
->action(function (string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForPlatform, Document $user, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb) {
$async = \strval($async) === 'true' || \strval($async) === '1';
if (!$async && !is_null($scheduledAt)) {
@@ -1955,7 +1987,7 @@ App::post('/v1/functions/:functionId/executions')
'userId' => $user->getId()
];
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => ScheduleExecutions::getSupportedResource(),
'resourceId' => $execution->getId(),
@@ -2290,9 +2322,9 @@ App::delete('/v1/functions/:functionId/executions/:executionId')
->param('executionId', '', new UID(), 'Execution ID.')
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForEvents')
->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, Database $dbForConsole, Event $queueForEvents) {
->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@@ -2318,7 +2350,7 @@ App::delete('/v1/functions/:functionId/executions/:executionId')
}
if ($status === 'scheduled') {
$schedule = $dbForConsole->findOne('schedules', [
$schedule = $dbForPlatform->findOne('schedules', [
Query::equal('resourceId', [$execution->getId()]),
Query::equal('resourceType', [ScheduleExecutions::getSupportedResource()]),
Query::equal('active', [true]),
@@ -2329,7 +2361,7 @@ App::delete('/v1/functions/:functionId/executions/:executionId')
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('active', false);
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
}
}
@@ -2362,8 +2394,8 @@ App::post('/v1/functions/:functionId/variables')
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (string $functionId, string $key, string $value, Response $response, Database $dbForProject, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $functionId, string $key, string $value, Response $response, Database $dbForProject, Database $dbForPlatform) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@@ -2396,12 +2428,12 @@ App::post('/v1/functions/:functionId/variables')
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
// Inform scheduler to pull the latest changes
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -2496,8 +2528,8 @@ App::put('/v1/functions/:functionId/variables/:variableId')
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (string $functionId, string $variableId, string $key, ?string $value, Response $response, Database $dbForProject, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $functionId, string $variableId, string $key, ?string $value, Response $response, Database $dbForProject, Database $dbForPlatform) {
$function = $dbForProject->getDocument('functions', $functionId);
@@ -2528,12 +2560,12 @@ App::put('/v1/functions/:functionId/variables/:variableId')
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
// Inform scheduler to pull the latest changes
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$response->dynamic($variable, Response::MODEL_VARIABLE);
});
@@ -2555,8 +2587,8 @@ App::delete('/v1/functions/:functionId/variables/:variableId')
->param('variableId', '', new UID(), 'Variable unique ID.', false)
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (string $functionId, string $variableId, Response $response, Database $dbForProject, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $functionId, string $variableId, Response $response, Database $dbForProject, Database $dbForPlatform) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@@ -2577,12 +2609,12 @@ App::delete('/v1/functions/:functionId/variables/:variableId')
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
// Inform scheduler to pull the latest changes
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$response->noContent();
});
+27 -27
View File
@@ -2649,11 +2649,11 @@ App::post('/v1/messaging/messages/email')
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, array $cc, array $bcc, array $attachments, bool $draft, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, array $cc, array $bcc, array $attachments, bool $draft, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -2742,7 +2742,7 @@ App::post('/v1/messaging/messages/email')
->setMessageId($message->getId());
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => 'message',
'resourceId' => $message->getId(),
@@ -2797,11 +2797,11 @@ App::post('/v1/messaging/messages/sms')
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, string $content, array $topics, array $users, array $targets, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, string $content, array $topics, array $users, array $targets, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -2859,7 +2859,7 @@ App::post('/v1/messaging/messages/sms')
->setMessageId($message->getId());
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => 'message',
'resourceId' => $message->getId(),
@@ -2923,11 +2923,11 @@ App::post('/v1/messaging/messages/push')
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, string $title, string $body, array $topics, array $users, array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, string $badge, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, string $title, string $body, array $topics, array $users, array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, string $badge, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -3036,7 +3036,7 @@ App::post('/v1/messaging/messages/push')
->setMessageId($message->getId());
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => 'message',
'resourceId' => $message->getId(),
@@ -3339,11 +3339,11 @@ App::patch('/v1/messaging/messages/email/:messageId')
->param('attachments', null, new ArrayList(new CompoundUID()), 'Array of compound ID strings of bucket IDs and file IDs to be attached to the email. They should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $subject, ?string $content, ?bool $draft, ?bool $html, ?array $cc, ?array $bcc, ?string $scheduledAt, ?array $attachments, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $subject, ?string $content, ?bool $draft, ?bool $html, ?array $cc, ?array $bcc, ?string $scheduledAt, ?array $attachments, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -3395,7 +3395,7 @@ App::patch('/v1/messaging/messages/email/:messageId')
}
if (\is_null($currentScheduledAt) && !\is_null($scheduledAt)) {
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => 'message',
'resourceId' => $message->getId(),
@@ -3410,7 +3410,7 @@ App::patch('/v1/messaging/messages/email/:messageId')
}
if (!\is_null($currentScheduledAt)) {
$schedule = $dbForConsole->getDocument('schedules', $message->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $message->getAttribute('scheduleId'));
$scheduledStatus = ($status ?? $message->getAttribute('status')) === MessageStatus::SCHEDULED;
if ($schedule->isEmpty()) {
@@ -3425,7 +3425,7 @@ App::patch('/v1/messaging/messages/email/:messageId')
$schedule->setAttribute('schedule', $scheduledAt);
}
$dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule);
$dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule);
}
if (!\is_null($scheduledAt)) {
@@ -3535,11 +3535,11 @@ App::patch('/v1/messaging/messages/sms/:messageId')
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $content, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $content, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -3591,7 +3591,7 @@ App::patch('/v1/messaging/messages/sms/:messageId')
}
if (\is_null($currentScheduledAt) && !\is_null($scheduledAt)) {
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => 'message',
'resourceId' => $message->getId(),
@@ -3606,7 +3606,7 @@ App::patch('/v1/messaging/messages/sms/:messageId')
}
if (!\is_null($currentScheduledAt)) {
$schedule = $dbForConsole->getDocument('schedules', $message->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $message->getAttribute('scheduleId'));
$scheduledStatus = ($status ?? $message->getAttribute('status')) === MessageStatus::SCHEDULED;
if ($schedule->isEmpty()) {
@@ -3621,7 +3621,7 @@ App::patch('/v1/messaging/messages/sms/:messageId')
$schedule->setAttribute('schedule', $scheduledAt);
}
$dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule);
$dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule);
}
if (!\is_null($scheduledAt)) {
@@ -3700,11 +3700,11 @@ App::patch('/v1/messaging/messages/push/:messageId')
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -3756,7 +3756,7 @@ App::patch('/v1/messaging/messages/push/:messageId')
}
if (\is_null($currentScheduledAt) && !\is_null($scheduledAt)) {
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => 'message',
'resourceId' => $message->getId(),
@@ -3771,7 +3771,7 @@ App::patch('/v1/messaging/messages/push/:messageId')
}
if (!\is_null($currentScheduledAt)) {
$schedule = $dbForConsole->getDocument('schedules', $message->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $message->getAttribute('scheduleId'));
$scheduledStatus = ($status ?? $message->getAttribute('status')) === MessageStatus::SCHEDULED;
if ($schedule->isEmpty()) {
@@ -3786,7 +3786,7 @@ App::patch('/v1/messaging/messages/push/:messageId')
$schedule->setAttribute('schedule', $scheduledAt);
}
$dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule);
$dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule);
}
if (!\is_null($scheduledAt)) {
@@ -3923,10 +3923,10 @@ App::delete('/v1/messaging/messages/:messageId')
->label('sdk.response.model', Response::MODEL_NONE)
->param('messageId', '', new UID(), 'Message ID.')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForEvents')
->inject('response')
->action(function (string $messageId, Database $dbForProject, Database $dbForConsole, Event $queueForEvents, Response $response) {
->action(function (string $messageId, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents, Response $response) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -3949,7 +3949,7 @@ App::delete('/v1/messaging/messages/:messageId')
if (!empty($scheduleId)) {
try {
$dbForConsole->deleteDocument('schedules', $scheduleId);
$dbForPlatform->deleteDocument('schedules', $scheduleId);
} catch (Exception) {
// Ignore
}
+1 -473
View File
@@ -1,17 +1,12 @@
<?php
use Appwrite\Auth\OAuth2\Firebase as OAuth2Firebase;
use Appwrite\Event\Event;
use Appwrite\Event\Migration;
use Appwrite\Extend\Exception;
use Appwrite\Permission;
use Appwrite\Role;
use Appwrite\Utopia\Database\Validator\Queries\Migrations;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
@@ -22,9 +17,7 @@ use Utopia\Migration\Sources\Appwrite;
use Utopia\Migration\Sources\Firebase;
use Utopia\Migration\Sources\NHost;
use Utopia\Migration\Sources\Supabase;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Host;
use Utopia\Validator\Integer;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
@@ -87,112 +80,9 @@ App::post('/v1/migrations/appwrite')
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::post('/v1/migrations/firebase/oauth')
->groups(['api', 'migrations'])
->desc('Migrate Firebase data (OAuth)')
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].create')
->label('audits.event', 'migration.create')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'createFirebaseOAuthMigration')
->label('sdk.description', '/docs/references/migrations/migration-firebase.md')
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MIGRATION)
->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate')
->param('projectId', '', new Text(65536), 'Project ID of the Firebase Project')
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->inject('project')
->inject('user')
->inject('queueForEvents')
->inject('queueForMigrations')
->inject('request')
->action(function (array $resources, string $projectId, Response $response, Database $dbForProject, Database $dbForConsole, Document $project, Document $user, Event $queueForEvents, Migration $queueForMigrations, Request $request) {
$firebase = new OAuth2Firebase(
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
);
$identity = $dbForConsole->findOne('identities', [
Query::equal('provider', ['firebase']),
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity->isEmpty()) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$accessToken = $identity->getAttribute('providerAccessToken');
$refreshToken = $identity->getAttribute('providerRefreshToken');
$accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry');
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
if ($isExpired) {
$firebase->refreshTokens($refreshToken);
$accessToken = $firebase->getAccessToken('');
$refreshToken = $firebase->getRefreshToken('');
$verificationId = $firebase->getUserID($accessToken);
if (empty($verificationId)) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.');
}
$identity = $identity
->setAttribute('providerAccessToken', $accessToken)
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry('')));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
}
if ($identity->getAttribute('secrets')) {
$serviceAccount = $identity->getAttribute('secrets');
} else {
$firebase->cleanupServiceAccounts($accessToken, $projectId);
$serviceAccount = $firebase->createServiceAccount($accessToken, $projectId);
$identity = $identity
->setAttribute('secrets', json_encode($serviceAccount));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
}
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
'stage' => 'init',
'source' => Firebase::getName(),
'destination' => Appwrite::getName(),
'credentials' => [
'serviceAccount' => json_encode($serviceAccount),
],
'resources' => $resources,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => []
]));
$queueForEvents->setParam('migrationId', $migration->getId());
// Trigger Transfer
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setUser($user)
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::post('/v1/migrations/firebase')
->groups(['api', 'migrations'])
->desc('Migrate Firebase data (Service Account)')
->desc('Migrate Firebase data')
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].create')
->label('audits.event', 'migration.create')
@@ -547,368 +437,6 @@ App::get('/v1/migrations/firebase/report')
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
});
App::get('/v1/migrations/firebase/report/oauth')
->groups(['api', 'migrations'])
->desc('Generate a report on Firebase data using OAuth')
->label('scope', 'migrations.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'getFirebaseReportOAuth')
->label('sdk.description', '/docs/references/migrations/migration-firebase-report.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MIGRATION_REPORT)
->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate')
->param('projectId', '', new Text(65536), 'Project ID')
->inject('response')
->inject('request')
->inject('user')
->inject('dbForConsole')
->action(function (array $resources, string $projectId, Response $response, Request $request, Document $user, Database $dbForConsole) {
$firebase = new OAuth2Firebase(
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
);
$identity = $dbForConsole->findOne('identities', [
Query::equal('provider', ['firebase']),
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity->isEmpty()) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$accessToken = $identity->getAttribute('providerAccessToken');
$refreshToken = $identity->getAttribute('providerRefreshToken');
$accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry');
if (empty($accessToken) || empty($refreshToken) || empty($accessTokenExpiry)) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
if (System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', '') === '' || System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', '') === '') {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
if ($isExpired) {
$firebase->refreshTokens($refreshToken);
$accessToken = $firebase->getAccessToken('');
$refreshToken = $firebase->getRefreshToken('');
$verificationId = $firebase->getUserID($accessToken);
if (empty($verificationId)) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.');
}
$identity = $identity
->setAttribute('providerAccessToken', $accessToken)
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry('')));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
}
// Get Service Account
if ($identity->getAttribute('secrets')) {
$serviceAccount = $identity->getAttribute('secrets');
} else {
$firebase->cleanupServiceAccounts($accessToken, $projectId);
$serviceAccount = $firebase->createServiceAccount($accessToken, $projectId);
$identity = $identity
->setAttribute('secrets', json_encode($serviceAccount));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
}
$firebase = new Firebase($serviceAccount);
try {
$report = $firebase->report($resources);
} catch (\Throwable $e) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Source Error: ' . $e->getMessage());
}
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
});
App::get('/v1/migrations/firebase/connect')
->desc('Authorize with Firebase')
->groups(['api', 'migrations'])
->label('scope', 'migrations.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'createFirebaseAuth')
->label('sdk.description', '')
->label('sdk.response.code', Response::STATUS_CODE_MOVED_PERMANENTLY)
->label('sdk.response.type', Response::CONTENT_TYPE_HTML)
->label('sdk.methodType', 'webAuth')
->label('sdk.hide', true)
->param('redirect', '', fn ($clients) => new Host($clients), 'URL to redirect back to your Firebase authorization. Only console hostnames are allowed.', true, ['clients'])
->param('projectId', '', new UID(), 'Project ID')
->inject('response')
->inject('request')
->inject('user')
->inject('dbForConsole')
->action(function (string $redirect, string $projectId, Response $response, Request $request, Document $user, Database $dbForConsole) {
$state = \json_encode([
'projectId' => $projectId,
'redirect' => $redirect,
]);
$prefs = $user->getAttribute('prefs', []);
$prefs['migrationState'] = $state;
$user->setAttribute('prefs', $prefs);
$dbForConsole->updateDocument('users', $user->getId(), $user);
$oauth2 = new OAuth2Firebase(
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
);
$url = $oauth2->getLoginURL();
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($url);
});
App::get('/v1/migrations/firebase/redirect')
->desc('Capture and receive data on Firebase authorization')
->groups(['api', 'migrations'])
->label('scope', 'public')
->label('error', __DIR__ . '/../../views/general/error.phtml')
->param('code', '', new Text(2048), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true)
->inject('user')
->inject('project')
->inject('request')
->inject('response')
->inject('dbForConsole')
->action(function (string $code, Document $user, Document $project, Request $request, Response $response, Database $dbForConsole) {
$state = $user['prefs']['migrationState'] ?? '{}';
$prefs['migrationState'] = '';
$user->setAttribute('prefs', $prefs);
$dbForConsole->updateDocument('users', $user->getId(), $user);
if (empty($state)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Installation requests from organisation members for the Appwrite Google App are currently unsupported.');
}
$state = \json_decode($state, true);
$redirect = $state['redirect'] ?? '';
$projectId = $state['projectId'] ?? '';
$project = $dbForConsole->getDocument('projects', $projectId);
if (empty($redirect)) {
$redirect = $request->getProtocol() . '://' . $request->getHostname() . '/console/project-$projectId/settings/migrations';
}
if ($project->isEmpty()) {
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirect);
return;
}
// OAuth Authroization
if (!empty($code)) {
$oauth2 = new OAuth2Firebase(
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
);
$accessToken = $oauth2->getAccessToken($code);
$refreshToken = $oauth2->getRefreshToken($code);
$accessTokenExpiry = $oauth2->getAccessTokenExpiry($code);
$email = $oauth2->getUserEmail($accessToken);
$oauth2ID = $oauth2->getUserID($accessToken);
if (empty($accessToken)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to get access token.');
}
if (empty($refreshToken)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to get refresh token.');
}
if (empty($accessTokenExpiry)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to get access token expiry.');
}
// Makes sure this email is not already used in another identity
$identity = $dbForConsole->findOne('identities', [
Query::equal('providerEmail', [$email]),
]);
if (!$identity->isEmpty()) {
if ($identity->getAttribute('userInternalId', '') !== $user->getInternalId()) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
}
if (!$identity->isEmpty()) {
$identity = $identity
->setAttribute('providerAccessToken', $accessToken)
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
} else {
$identity = $dbForConsole->createDocument('identities', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
],
'userInternalId' => $user->getInternalId(),
'userId' => $user->getId(),
'provider' => 'firebase',
'providerUid' => $oauth2ID,
'providerEmail' => $email,
'providerAccessToken' => $accessToken,
'providerRefreshToken' => $refreshToken,
'providerAccessTokenExpiry' => DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry),
]));
}
} else {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Missing OAuth2 code.');
}
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirect);
});
App::get('/v1/migrations/firebase/projects')
->desc('List Firebase projects')
->groups(['api', 'migrations'])
->label('scope', 'migrations.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'listFirebaseProjects')
->label('sdk.description', '')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST)
->inject('user')
->inject('response')
->inject('project')
->inject('dbForConsole')
->inject('request')
->action(function (Document $user, Response $response, Document $project, Database $dbForConsole, Request $request) {
$firebase = new OAuth2Firebase(
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
);
$identity = $dbForConsole->findOne('identities', [
Query::equal('provider', ['firebase']),
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity->isEmpty()) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$accessToken = $identity->getAttribute('providerAccessToken');
$refreshToken = $identity->getAttribute('providerRefreshToken');
$accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry');
if (empty($accessToken) || empty($refreshToken) || empty($accessTokenExpiry)) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
if (System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', '') === '' || System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', '') === '') {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
try {
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
if ($isExpired) {
try {
$firebase->refreshTokens($refreshToken);
} catch (\Throwable $e) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$accessToken = $firebase->getAccessToken('');
$refreshToken = $firebase->getRefreshToken('');
$verificationId = $firebase->getUserID($accessToken);
if (empty($verificationId)) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.');
}
$identity = $identity
->setAttribute('providerAccessToken', $accessToken)
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry('')));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
}
$projects = $firebase->getProjects($accessToken);
$output = [];
foreach ($projects as $project) {
$output[] = [
'displayName' => $project['displayName'],
'projectId' => $project['projectId'],
];
}
} catch (\Throwable $e) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$response->dynamic(new Document([
'projects' => $output,
'total' => count($output),
]), Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST);
});
App::get('/v1/migrations/firebase/deauthorize')
->desc('Revoke Appwrite\'s authorization to access Firebase projects')
->groups(['api', 'migrations'])
->label('scope', 'migrations.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'deleteFirebaseAuth')
->label('sdk.description', '')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->inject('user')
->inject('response')
->inject('dbForConsole')
->action(function (Document $user, Response $response, Database $dbForConsole) {
$identity = $dbForConsole->findOne('identities', [
Query::equal('provider', ['firebase']),
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity->isEmpty()) {
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Not authenticated with Firebase'); //TODO: Replace with USER_IDENTITY_NOT_FOUND
}
$dbForConsole->deleteDocument('identities', $identity->getId());
$response->noContent();
});
App::get('/v1/migrations/supabase/report')
->groups(['api', 'migrations'])
->desc('Generate a report on Supabase Data')
+4 -4
View File
@@ -325,8 +325,8 @@ App::post('/v1/project/variables')
->inject('project')
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (string $key, string $value, Document $project, Response $response, Database $dbForProject, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $key, string $value, Document $project, Response $response, Database $dbForProject, Database $dbForPlatform) {
$variableId = ID::unique();
$variable = new Document([
@@ -429,8 +429,8 @@ App::put('/v1/project/variables/:variableId')
->inject('project')
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (string $variableId, string $key, ?string $value, Document $project, Response $response, Database $dbForProject, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $variableId, string $key, ?string $value, Document $project, Response $response, Database $dbForProject, Database $dbForPlatform) {
$variable = $dbForProject->getDocument('variables', $variableId);
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
File diff suppressed because it is too large Load Diff
+35 -25
View File
@@ -11,6 +11,7 @@ use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
@@ -42,9 +43,9 @@ App::post('/v1/proxy/rules')
->inject('project')
->inject('queueForCertificates')
->inject('queueForEvents')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('dbForProject')
->action(function (string $domain, string $resourceType, string $resourceId, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForConsole, Database $dbForProject) {
->action(function (string $domain, string $resourceType, string $resourceId, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject) {
$mainDomain = System::getEnv('_APP_DOMAIN', '');
if ($domain === $mainDomain) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'You cannot assign your main domain to specific resource. Please use subdomain or a different domain.');
@@ -59,8 +60,15 @@ App::post('/v1/proxy/rules')
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.');
}
$ruleId = md5($domain);
$document = $dbForConsole->getDocument('rules', $ruleId);
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$document = $dbForPlatform->getDocument('rules', md5($domain));
} else {
$document = $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain]),
]);
}
if (!$document->isEmpty()) {
if ($document->getAttribute('projectId') === $project->getId()) {
@@ -101,7 +109,9 @@ App::post('/v1/proxy/rules')
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
}
$ruleId = md5($domain->get());
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
$rule = new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
@@ -135,7 +145,7 @@ App::post('/v1/proxy/rules')
}
$rule->setAttribute('status', $status);
$rule = $dbForConsole->createDocument('rules', $rule);
$rule = $dbForPlatform->createDocument('rules', $rule);
$queueForEvents->setParam('ruleId', $rule->getId());
@@ -161,8 +171,8 @@ App::get('/v1/proxy/rules')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (array $queries, string $search, Response $response, Document $project, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (array $queries, string $search, Response $response, Document $project, Database $dbForPlatform) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@@ -191,7 +201,7 @@ App::get('/v1/proxy/rules')
}
$ruleId = $cursor->getValue();
$cursorDocument = $dbForConsole->getDocument('rules', $ruleId);
$cursorDocument = $dbForPlatform->getDocument('rules', $ruleId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Rule '{$ruleId}' for the 'cursor' value not found.");
@@ -202,16 +212,16 @@ App::get('/v1/proxy/rules')
$filterQueries = Query::groupByType($queries)['filters'];
$rules = $dbForConsole->find('rules', $queries);
$rules = $dbForPlatform->find('rules', $queries);
foreach ($rules as $rule) {
$certificate = $dbForConsole->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$rule->setAttribute('logs', $certificate->getAttribute('logs', ''));
$rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', ''));
}
$response->dynamic(new Document([
'rules' => $rules,
'total' => $dbForConsole->count('rules', $filterQueries, APP_LIMIT_COUNT),
'total' => $dbForPlatform->count('rules', $filterQueries, APP_LIMIT_COUNT),
]), Response::MODEL_PROXY_RULE_LIST);
});
@@ -229,15 +239,15 @@ App::get('/v1/proxy/rules/:ruleId')
->param('ruleId', '', new UID(), 'Rule ID.')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $ruleId, Response $response, Document $project, Database $dbForConsole) {
$rule = $dbForConsole->getDocument('rules', $ruleId);
->inject('dbForPlatform')
->action(function (string $ruleId, Response $response, Document $project, Database $dbForPlatform) {
$rule = $dbForPlatform->getDocument('rules', $ruleId);
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getInternalId()) {
throw new Exception(Exception::RULE_NOT_FOUND);
}
$certificate = $dbForConsole->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$rule->setAttribute('logs', $certificate->getAttribute('logs', ''));
$rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', ''));
@@ -260,17 +270,17 @@ App::delete('/v1/proxy/rules/:ruleId')
->param('ruleId', '', new UID(), 'Rule ID.')
->inject('response')
->inject('project')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForDeletes')
->inject('queueForEvents')
->action(function (string $ruleId, Response $response, Document $project, Database $dbForConsole, Delete $queueForDeletes, Event $queueForEvents) {
$rule = $dbForConsole->getDocument('rules', $ruleId);
->action(function (string $ruleId, Response $response, Document $project, Database $dbForPlatform, Delete $queueForDeletes, Event $queueForEvents) {
$rule = $dbForPlatform->getDocument('rules', $ruleId);
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getInternalId()) {
throw new Exception(Exception::RULE_NOT_FOUND);
}
$dbForConsole->deleteDocument('rules', $rule->getId());
$dbForPlatform->deleteDocument('rules', $rule->getId());
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
@@ -299,10 +309,10 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
->inject('queueForCertificates')
->inject('queueForEvents')
->inject('project')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('log')
->action(function (string $ruleId, Response $response, Certificate $queueForCertificates, Event $queueForEvents, Document $project, Database $dbForConsole, Log $log) {
$rule = $dbForConsole->getDocument('rules', $ruleId);
->action(function (string $ruleId, Response $response, Certificate $queueForCertificates, Event $queueForEvents, Document $project, Database $dbForPlatform, Log $log) {
$rule = $dbForPlatform->getDocument('rules', $ruleId);
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getInternalId()) {
throw new Exception(Exception::RULE_NOT_FOUND);
@@ -332,7 +342,7 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
throw new Exception(Exception::RULE_VERIFICATION_FAILED);
}
$dbForConsole->updateDocument('rules', $rule->getId(), $rule->setAttribute('status', 'verifying'));
$dbForPlatform->updateDocument('rules', $rule->getId(), $rule->setAttribute('status', 'verifying'));
// Issue a TLS certificate when domain is verified
$queueForCertificates
@@ -343,7 +353,7 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
$queueForEvents->setParam('ruleId', $rule->getId());
$certificate = $dbForConsole->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$rule->setAttribute('logs', $certificate->getAttribute('logs', ''));
$response->dynamic($rule, Response::MODEL_PROXY_RULE);
+15 -6
View File
@@ -6,6 +6,7 @@ use Appwrite\Auth\Auth;
use Appwrite\ClamAV\Network;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Usage;
use Appwrite\Extend\Exception;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Utopia\Database\Validator\CustomId;
@@ -68,19 +69,20 @@ App::post('/v1/storage/buckets')
->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true)
->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan'])
->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true)
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD], true), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, string $compression, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $queueForEvents) {
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $queueForEvents) {
$bucketId = $bucketId === 'unique()' ? ID::unique() : $bucketId;
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions);
$compression ??= Compression::NONE;
$encryption ??= true;
try {
$files = (Config::getParam('collections', [])['buckets'] ?? [])['files'] ?? [];
if (empty($files)) {
@@ -254,13 +256,13 @@ App::put('/v1/storage/buckets/:bucketId')
->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true)
->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan'])
->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true)
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD], true), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, string $compression, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $queueForEvents) {
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $queueForEvents) {
$bucket = $dbForProject->getDocument('buckets', $bucketId);
if ($bucket->isEmpty()) {
@@ -273,6 +275,7 @@ App::put('/v1/storage/buckets/:bucketId')
$enabled ??= $bucket->getAttribute('enabled', true);
$encryption ??= $bucket->getAttribute('encryption', true);
$antivirus ??= $bucket->getAttribute('antivirus', true);
$compression ??= $bucket->getAttribute('compression', Compression::NONE);
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions);
@@ -884,7 +887,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
->inject('mode')
->inject('deviceForFiles')
->inject('deviceForLocal')
->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, Request $request, Response $response, Document $project, Database $dbForProject, string $mode, Device $deviceForFiles, Device $deviceForLocal) {
->inject('queueForUsage')
->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, Request $request, Response $response, Document $project, Database $dbForProject, string $mode, Device $deviceForFiles, Device $deviceForLocal, Usage $queueForUsage) {
if (!\extension_loaded('imagick')) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
@@ -1012,6 +1016,11 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'];
$queueForUsage
->addMetric(METRIC_FILES_TRANSFORMATIONS, 1)
->addMetric(str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_TRANSFORMATIONS), 1)
;
$response
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
->setContentType($contentType)
+66 -46
View File
@@ -805,8 +805,11 @@ App::get('/v1/teams/:teamId/memberships')
}, $membershipsPrivacy);
$memberships = array_map(function ($membership) use ($dbForProject, $team, $membershipsPrivacy) {
$user = !empty(array_filter($membershipsPrivacy))
? $dbForProject->getDocument('users', $membership->getAttribute('userId'))
: new Document();
if ($membershipsPrivacy['mfa']) {
$user = $dbForProject->getDocument('users', $membership->getAttribute('userId'));
$mfa = $user->getAttribute('mfa', false);
if ($mfa) {
@@ -888,9 +891,11 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
return $privacy || $isPrivilegedUser || $isAppUser;
}, $membershipsPrivacy);
if ($membershipsPrivacy['mfa']) {
$user = $dbForProject->getDocument('users', $membership->getAttribute('userId'));
$user = !empty(array_filter($membershipsPrivacy))
? $dbForProject->getDocument('users', $membership->getAttribute('userId'))
: new Document();
if ($membershipsPrivacy['mfa']) {
$mfa = $user->getAttribute('mfa', false);
if ($mfa) {
@@ -1055,7 +1060,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
throw new Exception(Exception::TEAM_INVITE_MISMATCH, 'Invite does not belong to current user (' . $user->getAttribute('email') . ')');
}
if ($user->isEmpty()) {
$hasSession = !$user->isEmpty();
if (!$hasSession) {
$user->setAttributes($dbForProject->getDocument('users', $userId)->getArrayCopy()); // Get user
}
@@ -1074,39 +1080,64 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
Authorization::skip(fn () => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true)));
// Log user in
// Create session for the user if not logged in
if (!$hasSession) {
Authorization::setRole(Role::user($user->getId())->toString());
Authorization::setRole(Role::user($user->getId())->toString());
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::addSeconds(new \DateTime(), $authDuration);
$secret = Auth::tokenGenerator();
$session = new Document(array_merge([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
],
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => $user->getAttribute('email'),
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'factors' => ['email'],
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => DateTime::addSeconds(new \DateTime(), $authDuration)
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::addSeconds(new \DateTime(), $authDuration);
$secret = Auth::tokenGenerator();
$session = new Document(array_merge([
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => $user->getAttribute('email'),
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'factors' => ['email'],
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => DateTime::addSeconds(new \DateTime(), $authDuration)
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
$session = $dbForProject->createDocument('sessions', $session);
$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())),
]));
Authorization::setRole(Role::user($userId)->toString());
$dbForProject->purgeCachedDocument('users', $user->getId());
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]));
}
Authorization::setRole(Role::user($userId)->toString());
$response
->addCookie(
name: Auth::$cookieName . '_legacy',
value: Auth::encodeSession($user->getId(), $secret),
expire: (new \DateTime($expire))->getTimestamp(),
path: '/',
domain: Config::getParam('cookieDomain'),
secure: ('https' === $protocol),
httponly: true
)
->addCookie(
name: Auth::$cookieName,
value: Auth::encodeSession($user->getId(), $secret),
expire: (new \DateTime($expire))->getTimestamp(),
path: '/',
domain: Config::getParam('cookieDomain'),
secure: ('https' === $protocol),
httponly: true,
sameSite: Config::getParam('cookieSamesite')
)
;
}
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership);
@@ -1120,22 +1151,11 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->setParam('membershipId', $membership->getId())
;
if (!Config::getParam('domainVerification')) {
$response
->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]))
;
}
$response
->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
;
$response->dynamic(
$membership
->setAttribute('teamName', $team->getAttribute('name'))
->setAttribute('userName', $user->getAttribute('name'))
->setAttribute('userEmail', $user->getAttribute('email')),
->setAttribute('teamName', $team->getAttribute('name'))
->setAttribute('userName', $user->getAttribute('name'))
->setAttribute('userEmail', $user->getAttribute('email')),
Response::MODEL_MEMBERSHIP
);
});
+5 -1
View File
@@ -1804,6 +1804,7 @@ App::post('/v1/users/:userId/sessions')
'provider' => Auth::SESSION_PROVIDER_SERVER,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'factors' => ['server'],
'ip' => $request->getIP(),
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => $expire,
@@ -1816,8 +1817,11 @@ App::post('/v1/users/:userId/sessions')
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
$session = $dbForProject->createDocument('sessions', $session);
$dbForProject->purgeCachedDocument('users', $user->getId());
$session
->setAttribute('secret', $secret)
->setAttribute('secret', Auth::encodeSession($user->getId(), $secret))
->setAttribute('countryName', $countryName);
$queueForEvents
+64 -64
View File
@@ -42,7 +42,7 @@ use Utopia\VCS\Exception\RepositoryNotFound;
use function Swoole\Coroutine\batch;
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForConsole, Build $queueForBuilds, callable $getProjectDB, Request $request) {
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForPlatform, Build $queueForBuilds, callable $getProjectDB, Request $request) {
$errors = [];
foreach ($repositories as $resource) {
try {
@@ -53,7 +53,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
}
$projectId = $resource->getAttribute('projectId');
$project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId));
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
$dbForProject = $getProjectDB($project);
$functionId = $resource->getAttribute('resourceId');
@@ -104,7 +104,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$latestCommentId = '';
if (!empty($providerPullRequestId) && $function->getAttribute('providerSilentMode', false) === false) {
$latestComment = Authorization::skip(fn () => $dbForConsole->findOne('vcsComments', [
$latestComment = Authorization::skip(fn () => $dbForPlatform->findOne('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerPullRequestId', [$providerPullRequestId]),
Query::orderDesc('$createdAt'),
@@ -125,7 +125,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
if (!empty($latestCommentId)) {
$teamId = $project->getAttribute('teamId', '');
$latestComment = Authorization::skip(fn () => $dbForConsole->createDocument('vcsComments', new Document([
$latestComment = Authorization::skip(fn () => $dbForPlatform->createDocument('vcsComments', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
@@ -146,7 +146,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
}
}
} elseif (!empty($providerBranch)) {
$latestComments = Authorization::skip(fn () => $dbForConsole->find('vcsComments', [
$latestComments = Authorization::skip(fn () => $dbForPlatform->find('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerBranch', [$providerBranch]),
Query::orderDesc('$createdAt'),
@@ -319,8 +319,8 @@ App::get('/v1/vcs/github/callback')
->inject('project')
->inject('request')
->inject('response')
->inject('dbForConsole')
->action(function (string $providerInstallationId, string $setupAction, string $state, string $code, GitHub $github, Document $user, Document $project, Request $request, Response $response, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $providerInstallationId, string $setupAction, string $state, string $code, GitHub $github, Document $user, Document $project, Request $request, Response $response, Database $dbForPlatform) {
if (empty($state)) {
$error = 'Installation requests from organisation members for the Appwrite GitHub App are currently unsupported. To proceed with the installation, login to the Appwrite Console and install the GitHub App.';
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $error);
@@ -339,7 +339,7 @@ App::get('/v1/vcs/github/callback')
$redirectSuccess = $state['success'] ?? '';
$redirectFailure = $state['failure'] ?? '';
$project = $dbForConsole->getDocument('projects', $projectId);
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
$error = 'Project with the ID from state could not be found.';
@@ -368,7 +368,7 @@ App::get('/v1/vcs/github/callback')
$oauth2ID = $oauth2->getUserID($accessToken);
// Makes sure this email is not already used in another identity
$identity = $dbForConsole->findOne('identities', [
$identity = $dbForPlatform->findOne('identities', [
Query::equal('providerEmail', [$email]),
]);
if (!$identity->isEmpty()) {
@@ -381,9 +381,9 @@ App::get('/v1/vcs/github/callback')
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
$dbForPlatform->updateDocument('identities', $identity->getId(), $identity);
} else {
$identity = $dbForConsole->createDocument('identities', new Document([
$identity = $dbForPlatform->createDocument('identities', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
@@ -411,7 +411,7 @@ App::get('/v1/vcs/github/callback')
$projectInternalId = $project->getInternalId();
$installation = $dbForConsole->findOne('installations', [
$installation = $dbForPlatform->findOne('installations', [
Query::equal('providerInstallationId', [$providerInstallationId]),
Query::equal('projectInternalId', [$projectInternalId])
]);
@@ -436,12 +436,12 @@ App::get('/v1/vcs/github/callback')
'personal' => $personalSlug === $owner
]);
$installation = $dbForConsole->createDocument('installations', $installation);
$installation = $dbForPlatform->createDocument('installations', $installation);
} else {
$installation = $installation
->setAttribute('organization', $owner)
->setAttribute('personal', $personalSlug === $owner);
$installation = $dbForConsole->updateDocument('installations', $installation->getId(), $installation);
$installation = $dbForPlatform->updateDocument('installations', $installation->getId(), $installation);
}
} else {
$error = 'Installation of the Appwrite GitHub App on organization accounts is restricted to organization owners. As a member of the organization, you do not have the necessary permissions to install this GitHub App. Please contact the organization owner to create the installation from the Appwrite console.';
@@ -480,9 +480,9 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, string $providerRepositoryId, string $providerRootDirectory, GitHub $github, Response $response, Document $project, Database $dbForConsole) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->inject('dbForPlatform')
->action(function (string $installationId, string $providerRepositoryId, string $providerRootDirectory, GitHub $github, Response $response, Document $project, Database $dbForPlatform) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -541,9 +541,9 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:pr
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, string $providerRepositoryId, string $providerRootDirectory, GitHub $github, Response $response, Document $project, Database $dbForConsole) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->inject('dbForPlatform')
->action(function (string $installationId, string $providerRepositoryId, string $providerRootDirectory, GitHub $github, Response $response, Document $project, Database $dbForPlatform) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -612,13 +612,13 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, string $search, GitHub $github, Response $response, Document $project, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $installationId, string $search, GitHub $github, Response $response, Document $project, Database $dbForPlatform) {
if (empty($search)) {
$search = "";
}
$installation = $dbForConsole->getDocument('installations', $installationId);
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -709,9 +709,9 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories')
->inject('user')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, string $name, bool $private, GitHub $github, Document $user, Response $response, Document $project, Database $dbForConsole) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->inject('dbForPlatform')
->action(function (string $installationId, string $name, bool $private, GitHub $github, Document $user, Response $response, Document $project, Database $dbForPlatform) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -720,7 +720,7 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories')
if ($installation->getAttribute('personal', false) === true) {
$oauth2 = new OAuth2Github(System::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''), System::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', ''), "");
$identity = $dbForConsole->findOne('identities', [
$identity = $dbForPlatform->findOne('identities', [
Query::equal('provider', ['github']),
Query::equal('userInternalId', [$user->getInternalId()]),
]);
@@ -750,7 +750,7 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories')
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry('')));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
$dbForPlatform->updateDocument('identities', $identity->getId(), $identity);
}
try {
@@ -808,9 +808,9 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, string $providerRepositoryId, GitHub $github, Response $response, Document $project, Database $dbForConsole) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->inject('dbForPlatform')
->action(function (string $installationId, string $providerRepositoryId, GitHub $github, Response $response, Document $project, Database $dbForPlatform) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -857,9 +857,9 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, string $providerRepositoryId, GitHub $github, Response $response, Document $project, Database $dbForConsole) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->inject('dbForPlatform')
->action(function (string $installationId, string $providerRepositoryId, GitHub $github, Response $response, Document $project, Database $dbForPlatform) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -897,11 +897,11 @@ App::post('/v1/vcs/github/events')
->inject('gitHub')
->inject('request')
->inject('response')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForBuilds')
->action(
function (GitHub $github, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
$payload = $request->getRawPayload();
$signatureRemote = $request->getHeader('x-hub-signature-256', '');
$signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', '');
@@ -935,36 +935,36 @@ App::post('/v1/vcs/github/events')
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
//find functionId from functions table
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::limit(100),
]));
// create new deployment only on push and not when branch is created
if (!$providerBranchCreated) {
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForConsole, $queueForBuilds, $getProjectDB, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
}
} elseif ($event == $github::EVENT_INSTALLATION) {
if ($parsedPayload["action"] == "deleted") {
// TODO: Use worker for this job instead (update function as well)
$providerInstallationId = $parsedPayload["installationId"];
$installations = $dbForConsole->find('installations', [
$installations = $dbForPlatform->find('installations', [
Query::equal('providerInstallationId', [$providerInstallationId]),
Query::limit(1000)
]);
foreach ($installations as $installation) {
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('installationInternalId', [$installation->getInternalId()]),
Query::limit(1000)
]));
foreach ($repositories as $repository) {
Authorization::skip(fn () => $dbForConsole->deleteDocument('repositories', $repository->getId()));
Authorization::skip(fn () => $dbForPlatform->deleteDocument('repositories', $repository->getId()));
}
$dbForConsole->deleteDocument('installations', $installation->getId());
$dbForPlatform->deleteDocument('installations', $installation->getId());
}
}
} elseif ($event == $github::EVENT_PULL_REQUEST) {
@@ -993,12 +993,12 @@ App::post('/v1/vcs/github/events')
$providerCommitAuthor = $commitDetails["commitAuthor"] ?? '';
$providerCommitMessage = $commitDetails["commitMessage"] ?? '';
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt')
]));
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForConsole, $queueForBuilds, $getProjectDB, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
} elseif ($parsedPayload["action"] == "closed") {
// Allowed external contributions cleanup
@@ -1007,7 +1007,7 @@ App::post('/v1/vcs/github/events')
$external = $parsedPayload["external"] ?? true;
if ($external) {
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt')
]));
@@ -1018,7 +1018,7 @@ App::post('/v1/vcs/github/events')
if (\in_array($providerPullRequestId, $providerPullRequestIds)) {
$providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]);
$repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds);
$repository = Authorization::skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository));
$repository = Authorization::skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
}
}
}
@@ -1045,8 +1045,8 @@ App::get('/v1/vcs/installations')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (array $queries, string $search, Response $response, Document $project, Database $dbForProject, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (array $queries, string $search, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@@ -1075,7 +1075,7 @@ App::get('/v1/vcs/installations')
}
$installationId = $cursor->getValue();
$cursorDocument = $dbForConsole->getDocument('installations', $installationId);
$cursorDocument = $dbForPlatform->getDocument('installations', $installationId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Installation '{$installationId}' for the 'cursor' value not found.");
@@ -1086,8 +1086,8 @@ App::get('/v1/vcs/installations')
$filterQueries = Query::groupByType($queries)['filters'];
$results = $dbForConsole->find('installations', $queries);
$total = $dbForConsole->count('installations', $filterQueries, APP_LIMIT_COUNT);
$results = $dbForPlatform->find('installations', $queries);
$total = $dbForPlatform->count('installations', $filterQueries, APP_LIMIT_COUNT);
$response->dynamic(new Document([
'installations' => $results,
@@ -1109,9 +1109,9 @@ App::get('/v1/vcs/installations/:installationId')
->param('installationId', '', new Text(256), 'Installation Id')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, Response $response, Document $project, Database $dbForConsole) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->inject('dbForPlatform')
->action(function (string $installationId, Response $response, Document $project, Database $dbForPlatform) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation === false || $installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -1137,16 +1137,16 @@ App::delete('/v1/vcs/installations/:installationId')
->param('installationId', '', new Text(256), 'Installation Id')
->inject('response')
->inject('project')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForDeletes')
->action(function (string $installationId, Response $response, Document $project, Database $dbForConsole, Delete $queueForDeletes) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->action(function (string $installationId, Response $response, Document $project, Database $dbForPlatform, Delete $queueForDeletes) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
if (!$dbForConsole->deleteDocument('installations', $installation->getId())) {
if (!$dbForPlatform->deleteDocument('installations', $installation->getId())) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove installation from DB');
}
@@ -1174,17 +1174,17 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
->inject('request')
->inject('response')
->inject('project')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForBuilds')
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$repository = Authorization::skip(fn () => $dbForConsole->getDocument('repositories', $repositoryId, [
$repository = Authorization::skip(fn () => $dbForPlatform->getDocument('repositories', $repositoryId, [
Query::equal('projectInternalId', [$project->getInternalId()])
]));
@@ -1201,7 +1201,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
// TODO: Delete from array when PR is closed
$repository = Authorization::skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository));
$repository = Authorization::skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
@@ -1225,7 +1225,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
$providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? '';
$providerCommitHash = $pullRequestResponse['head']['sha'] ?? '';
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForConsole, $queueForBuilds, $getProjectDB, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
$response->noContent();
});
+94 -51
View File
@@ -29,6 +29,7 @@ use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Domains\Domain;
use Utopia\DSN\DSN;
@@ -45,13 +46,26 @@ Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(App $utopia, Database $dbForConsole, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked)
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname)
{
$utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml');
$host = $request->getHostname() ?? '';
if (!empty($previewHostname)) {
$host = $previewHostname;
}
$route = Authorization::skip(fn () => $dbForConsole->getDocument('rules', md5($host)));
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$route = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($host)));
} else {
$route = Authorization::skip(
fn () => $dbForPlatform->find('rules', [
Query::equal('domain', [$host]),
Query::limit(1)
])
)[0] ?? new Document();
}
if ($route->isEmpty()) {
if ($host === System::getEnv('_APP_DOMAIN_FUNCTIONS', '')) {
@@ -63,7 +77,7 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
}
if (System::getEnv('_APP_OPTIONS_ROUTER_PROTECTION', 'disabled') === 'enabled') {
if ($host !== 'localhost' && $host !== APP_HOSTNAME_INTERNAL) { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations
if ($host !== 'localhost' && $host !== APP_HOSTNAME_INTERNAL && $host !== System::getEnv('_APP_CONSOLE_DOMAIN', '')) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Router protection does not allow accessing Appwrite over this domain. Please add it as custom domain to your project or disable _APP_OPTIONS_ROUTER_PROTECTION environment variable.');
}
}
@@ -75,8 +89,17 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
$projectId = $route->getAttribute('projectId');
$project = Authorization::skip(
fn () => $dbForConsole->getDocument('projects', $projectId)
fn () => $dbForPlatform->getDocument('projects', $projectId)
);
if (!$project->isEmpty() && $project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$project->setAttribute('accessedAt', DateTime::now());
Authorization::skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
}
}
if (array_key_exists('proxy', $project->getAttribute('services', []))) {
$status = $project->getAttribute('services', [])['proxy'];
if (!$status) {
@@ -121,7 +144,7 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
$requestHeaders = $request->getHeaders();
$project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId));
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
$dbForProject = $getProjectDB($project);
@@ -346,26 +369,6 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
throw $th;
}
} finally {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$queueForUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize())
->addMetric(METRIC_EXECUTIONS, 1)
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1)
->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->setProject($project)
->trigger()
;
$queueForFunctions
->setType(Func::TYPE_ASYNC_WRITE)
->setExecution($execution)
@@ -400,6 +403,26 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
->setStatusCode($execution['responseStatusCode'] ?? 200)
->send($body);
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$queueForUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize())
->addMetric(METRIC_EXECUTIONS, 1)
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1)
->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->setProject($project)
->trigger()
;
return true;
} elseif ($type === 'api') {
$utopia->getRoute()?->label('error', '');
@@ -445,7 +468,7 @@ App::init()
->inject('response')
->inject('console')
->inject('project')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('locale')
->inject('localeCodes')
@@ -456,15 +479,16 @@ App::init()
->inject('queueForCertificates')
->inject('queueForFunctions')
->inject('isResourceBlocked')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, callable $getProjectDB, Locale $locale, array $localeCodes, array $clients, Reader $geodb, Usage $queueForUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, callable $isResourceBlocked) {
->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, array $clients, Reader $geodb, Usage $queueForUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, callable $isResourceBlocked, string $previewHostname) {
/*
* Appwrite Router
*/
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain
if ($host !== $mainDomain) {
if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked)) {
if ($host !== $mainDomain || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) {
return;
}
}
@@ -512,18 +536,31 @@ App::init()
if (!empty($envDomain) && $envDomain !== 'localhost') {
$mainDomain = $envDomain;
} else {
$domainDocument = $dbForConsole->getDocument('rules', md5($envDomain));
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$domainDocument = $dbForPlatform->getDocument('rules', md5($envDomain));
} else {
$domainDocument = $dbForPlatform->findOne('rules', [Query::orderAsc('$id')]);
}
$mainDomain = !$domainDocument->isEmpty() ? $domainDocument->getAttribute('domain') : $domain->get();
}
if ($mainDomain !== $domain->get()) {
Console::warning($domain->get() . ' is not a main domain. Skipping SSL certificate generation.');
} else {
$domainDocument = $dbForConsole->getDocument('rules', md5($domain->get()));
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$domainDocument = $dbForPlatform->getDocument('rules', md5($domain->get()));
} else {
$domainDocument = $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain->get()])
]);
}
if ($domainDocument->isEmpty()) {
$domainDocument = new Document([
'$id' => md5($domain->get()),
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
'$id' => System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(),
'domain' => $domain->get(),
'resourceType' => 'api',
'status' => 'verifying',
@@ -531,7 +568,7 @@ App::init()
'projectInternalId' => 'console'
]);
$domainDocument = $dbForConsole->createDocument('rules', $domainDocument);
$domainDocument = $dbForPlatform->createDocument('rules', $domainDocument);
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
@@ -667,22 +704,23 @@ App::options()
->inject('swooleRequest')
->inject('request')
->inject('response')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForUsage')
->inject('queueForFunctions')
->inject('geodb')
->inject('isResourceBlocked')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked) {
->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname) {
/*
* Appwrite Router
*/
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain
if ($host !== $mainDomain) {
if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked)) {
if ($host !== $mainDomain || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) {
return;
}
}
@@ -797,7 +835,7 @@ App::error()
$adapter = new Sentry($projectId, $key, $host);
$logger = new Logger($adapter);
$logger->setSample(0.04);
$logger->setSample(0.01);
$publish = true;
} else {
throw new \Exception('Invalid experimental logging provider');
@@ -837,6 +875,8 @@ App::error()
if (isset($user) && !$user->isEmpty()) {
$log->setUser(new User($user->getId()));
} else {
$log->setUser(new User('guest-' . hash('sha256', $request->getIP())));
}
try {
@@ -847,7 +887,7 @@ App::error()
}
$log->setNamespace("http");
$log->setServer(\gethostname());
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
@@ -868,6 +908,7 @@ App::error()
$action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD");
$log->setAction($action);
$log->addTag('service', $action);
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
@@ -962,22 +1003,23 @@ App::get('/robots.txt')
->inject('swooleRequest')
->inject('request')
->inject('response')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForUsage')
->inject('queueForFunctions')
->inject('geodb')
->inject('isResourceBlocked')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked) {
->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname) {
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
if ($host === $mainDomain || $host === 'localhost') {
if (($host === $mainDomain || $host === 'localhost') && empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked);
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname);
}
});
@@ -989,22 +1031,23 @@ App::get('/humans.txt')
->inject('swooleRequest')
->inject('request')
->inject('response')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForUsage')
->inject('queueForFunctions')
->inject('geodb')
->inject('isResourceBlocked')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked) {
->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname) {
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
if ($host === $mainDomain || $host === 'localhost') {
if (($host === $mainDomain || $host === 'localhost') && empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked);
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname);
}
});
@@ -1068,9 +1111,9 @@ App::get('/v1/ping')
->label('event', 'projects.[projectId].ping')
->inject('response')
->inject('project')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForEvents')
->action(function (Response $response, Document $project, Database $dbForConsole, Event $queueForEvents) {
->action(function (Response $response, Document $project, Database $dbForPlatform, Event $queueForEvents) {
if ($project->isEmpty()) {
throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND);
}
@@ -1082,8 +1125,8 @@ App::get('/v1/ping')
->setAttribute('pingCount', $pingCount)
->setAttribute('pingedAt', $pingedAt);
Authorization::skip(function () use ($dbForConsole, $project) {
$dbForConsole->updateDocument('projects', $project->getId(), $project);
Authorization::skip(function () use ($dbForPlatform, $project) {
$dbForPlatform->updateDocument('projects', $project->getId(), $project);
});
$queueForEvents
+9 -9
View File
@@ -162,15 +162,15 @@ App::post('/v1/mock/api-key-unprefixed')
->label('docs', false)
->param('projectId', '', new UID(), 'Project ID.')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, Response $response, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $projectId, Response $response, Database $dbForPlatform) {
$isDevelopment = System::getEnv('_APP_ENV', 'development') === 'development';
if (!$isDevelopment) {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
}
$project = $dbForConsole->getDocument('projects', $projectId);
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
@@ -195,9 +195,9 @@ App::post('/v1/mock/api-key-unprefixed')
'secret' => \bin2hex(\random_bytes(128)),
]);
$key = $dbForConsole->createDocument('keys', $key);
$key = $dbForPlatform->createDocument('keys', $key);
$dbForConsole->purgeCachedDocument('projects', $project->getId());
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -214,15 +214,15 @@ App::get('/v1/mock/github/callback')
->inject('gitHub')
->inject('project')
->inject('response')
->inject('dbForConsole')
->action(function (string $providerInstallationId, string $projectId, GitHub $github, Document $project, Response $response, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $providerInstallationId, string $projectId, GitHub $github, Document $project, Response $response, Database $dbForPlatform) {
$isDevelopment = System::getEnv('_APP_ENV', 'development') === 'development';
if (!$isDevelopment) {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
}
$project = $dbForConsole->getDocument('projects', $projectId);
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
$error = 'Project with the ID from state could not be found.';
@@ -256,7 +256,7 @@ App::get('/v1/mock/github/callback')
'personal' => false
]);
$installation = $dbForConsole->createDocument('installations', $installation);
$installation = $dbForPlatform->createDocument('installations', $installation);
}
$response->json([
+39 -42
View File
@@ -19,7 +19,6 @@ use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\Database\TimeLimit;
use Utopia\App;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
@@ -184,14 +183,15 @@ App::init()
->groups(['api'])
->inject('utopia')
->inject('request')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('dbForProject')
->inject('project')
->inject('user')
->inject('session')
->inject('servers')
->inject('mode')
->inject('team')
->action(function (App $utopia, Request $request, Database $dbForConsole, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team) {
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team) {
$route = $utopia->getRoute();
if ($project->isEmpty()) {
@@ -283,8 +283,8 @@ App::init()
$accessedAt = $key->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
$key->setAttribute('accessedAt', DateTime::now());
$dbForConsole->updateDocument('keys', $key->getId(), $key);
$dbForConsole->purgeCachedDocument('projects', $project->getId());
$dbForPlatform->updateDocument('keys', $key->getId(), $key);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
$sdkValidator = new WhiteList($servers, true);
@@ -297,8 +297,8 @@ App::init()
/** Update access time as well */
$key->setAttribute('accessedAt', Datetime::now());
$dbForConsole->updateDocument('keys', $key->getId(), $key);
$dbForConsole->purgeCachedDocument('projects', $project->getId());
$dbForPlatform->updateDocument('keys', $key->getId(), $key);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
}
}
@@ -335,6 +335,33 @@ App::init()
Authorization::setRole($authRole);
}
/**
* Update project last activity
*/
if (!$project->isEmpty() && $project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$project->setAttribute('accessedAt', DateTime::now());
Authorization::skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
}
}
/**
* Update user last activity
*/
if (!empty($user->getId())) {
$accessedAt = $user->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
$user->setAttribute('accessedAt', DateTime::now());
if (APP_MODE_ADMIN !== $mode) {
$dbForProject->updateDocument('users', $user->getId(), $user);
} else {
$dbForPlatform->updateDocument('users', $user->getId(), $user);
}
}
}
/** Do not allow access to disabled services */
$service = $route->getLabel('sdk.namespace', '');
if (!empty($service)) {
@@ -392,8 +419,9 @@ App::init()
->inject('queueForBuilds')
->inject('queueForUsage')
->inject('dbForProject')
->inject('timelimit')
->inject('mode')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Connection $queue, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Usage $queueForUsage, Database $dbForProject, string $mode) use ($usageDatabaseListener, $eventDatabaseListener) {
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Connection $queue, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Usage $queueForUsage, Database $dbForProject, callable $timelimit, string $mode) use ($usageDatabaseListener, $eventDatabaseListener) {
$route = $utopia->getRoute();
@@ -416,7 +444,7 @@ App::init()
foreach ($abuseKeyLabel as $abuseKey) {
$start = $request->getContentRangeStart();
$end = $request->getContentRangeEnd();
$timeLimit = new TimeLimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600), $dbForProject);
$timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600));
$timeLimit
->setParam('{projectId}', $project->getId())
->setParam('{userId}', $user->getId())
@@ -444,7 +472,7 @@ App::init()
$abuse = new Abuse($timeLimit);
$remaining = $timeLimit->remaining();
$limit = $timeLimit->limit();
$time = (new \DateTime($timeLimit->time()))->getTimestamp() + $route->getLabel('abuse-time', 3600);
$time = $timeLimit->time() + $route->getLabel('abuse-time', 3600);
if ($limit && ($remaining < $closestLimit || is_null($closestLimit))) {
$closestLimit = $remaining;
@@ -641,9 +669,7 @@ App::shutdown()
->inject('queueForWebhooks')
->inject('queueForRealtime')
->inject('dbForProject')
->inject('mode')
->inject('dbForConsole')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, Usage $queueForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, string $mode, Database $dbForConsole) use ($parseLabel) {
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, Usage $queueForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -763,8 +789,6 @@ App::shutdown()
}
}
if ($project->getId() !== 'console') {
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
$fileSize = 0;
@@ -783,33 +807,6 @@ App::shutdown()
->setProject($project)
->trigger();
}
/**
* Update project last activity
*/
if (!$project->isEmpty() && $project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$project->setAttribute('accessedAt', DateTime::now());
Authorization::skip(fn () => $dbForConsole->updateDocument('projects', $project->getId(), $project));
}
}
/**
* Update user last activity
*/
if (!$user->isEmpty()) {
$accessedAt = $user->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
$user->setAttribute('accessedAt', DateTime::now());
if (APP_MODE_ADMIN !== $mode) {
$dbForProject->updateDocument('users', $user->getId(), $user);
} else {
$dbForConsole->updateDocument('users', $user->getId(), $user);
}
}
}
});
App::init()
+222 -75
View File
@@ -9,16 +9,19 @@ use Swoole\Http\Request as SwooleRequest;
use Swoole\Http\Response as SwooleResponse;
use Swoole\Http\Server;
use Swoole\Process;
use Utopia\Abuse\Adapters\Database\TimeLimit;
use Swoole\Table;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Logger\Log\User;
@@ -26,6 +29,12 @@ use Utopia\Pools\Group;
use Utopia\Swoole\Files;
use Utopia\System\System;
const DOMAIN_SYNC_TIMER = 30; // 30 seconds
$domains = new Table(1_000_000); // 1 million rows
$domains->column('value', Table::TYPE_INT, 1);
$domains->create();
$http = new Server(
host: "0.0.0.0",
port: System::getEnv('PORT', 80),
@@ -33,15 +42,17 @@ $http = new Server(
);
$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
$workerNumber = swoole_cpu_num() * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$http
->set([
'worker_num' => $workerNumber,
'worker_num' => $totalWorkers,
'dispatch_func' => 'dispatch',
'open_http2_protocol' => true,
'http_compression' => false,
'package_max_length' => $payloadSize,
'buffer_output_size' => $payloadSize,
'task_worker_num' => 1, // required for the task to fetch domains background
]);
$http->on(Constant::EVENT_WORKER_START, function ($server, $workerId) {
@@ -56,12 +67,97 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server, $workerId) {
Console::success('Reload completed...');
});
/**
* Assigns HTTP requests to worker threads by analyzing its payload/content.
*
* Routes requests as 'safe' or 'risky' based on specific content patterns (like POST actions or certain domains)
* to optimize load distribution between the workers. Utilizes `$safeThreadsPercent` to manage risk by assigning
* 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 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
{
global $totalWorkers, $domains;
// If data is not set we can send request to any worker
// first we try to pick idle worker, if not we randomly pick a worker
if ($data === null) {
for ($i = 0; $i < $totalWorkers; $i++) {
if ($server->getWorkerStatus($i) === SWOOLE_WORKER_IDLE) {
return $i;
}
}
return rand(0, $totalWorkers - 1);
}
$riskyWorkersPercent = intval(System::getEnv('_APP_RISKY_WORKERS_PERCENT', 80)) / 100; // Decimal form 0 to 1
// Each worker has numeric ID, starting from 0 and incrementing
// From 0 to riskyWorkers, we consider safe workers
// From riskyWorkers to totalWorkers, we consider risky workers
$riskyWorkers = (int) floor($totalWorkers * $riskyWorkersPercent); // Absolute amount of risky workers
$domain = '';
// max up to 3 as first line has request details and second line has host
$lines = explode("\n", $data, 3);
$request = $lines[0];
if (count($lines) > 1) {
$domain = trim(explode('Host: ', $lines[1])[1]);
}
// Sync executions are considered risky
$risky = false;
if (str_starts_with($request, 'POST') && str_contains($request, '/executions')) {
$risky = true;
} elseif (str_ends_with($domain, System::getEnv('_APP_DOMAIN_FUNCTIONS'))) {
$risky = true;
} elseif ($domains->get(md5($domain), 'value') === 1) {
// executions request coming from custom domain
$risky = true;
}
if ($risky) {
// If risky request, only consider risky workers
for ($j = $riskyWorkers; $j < $totalWorkers; $j++) {
/** Reference https://openswoole.com/docs/modules/swoole-server-getWorkerStatus#description */
if ($server->getWorkerStatus($j) === SWOOLE_WORKER_IDLE) {
// If idle worker found, give to him
return $j;
}
}
// If no idle workers, give to random risky worker
$worker = rand($riskyWorkers, $totalWorkers - 1);
Console::warning("swoole_dispatch: Risky branch: did not find a idle worker, picking random worker {$worker}");
return $worker;
}
// If safe request, give to any idle worker
// Its fine to pick risky worker here, because it's idle. Idle is never actually risky
for ($i = 0; $i < $totalWorkers; $i++) {
if ($server->getWorkerStatus($i) === SWOOLE_WORKER_IDLE) {
return $i;
}
}
// If no idle worker found, give to random safe worker
// We avoid risky workers here, as it could be in work - not idle. Thats exactly when they are risky.
$worker = rand(0, $riskyWorkers - 1);
Console::warning("swoole_dispatch: Non-risky branch: did not find a idle worker, picking random worker {$worker}");
return $worker;
}
include __DIR__ . '/controllers/general.php';
$http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $register) {
$app = new App('UTC');
$app->setCompression(true);
$app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB
go(function () use ($register, $app) {
$pools = $register->get('pools');
@@ -76,8 +172,8 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
do {
try {
$attempts++;
$dbForConsole = $app->getResource('dbForConsole');
/** @var Utopia\Database\Database $dbForConsole */
$dbForPlatform = $app->getResource('dbForPlatform');
/** @var Utopia\Database\Database $dbForPlatform */
break; // leave the do-while if successful
} catch (\Throwable $e) {
Console::warning("Database not ready. Retrying connection ({$attempts})...");
@@ -91,22 +187,17 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
Console::success('[Setup] - Server database init started...');
try {
Console::success('[Setup] - Creating database: appwrite...');
$dbForConsole->create();
} catch (\Throwable $e) {
Console::success('[Setup] - Creating console database...');
$dbForPlatform->create();
} catch (Duplicate) {
Console::success('[Setup] - Skip: metadata table already exists');
}
if ($dbForConsole->getCollection(Audit::COLLECTION)->isEmpty()) {
$audit = new Audit($dbForConsole);
if ($dbForPlatform->getCollection(Audit::COLLECTION)->isEmpty()) {
$audit = new Audit($dbForPlatform);
$audit->setup();
}
if ($dbForConsole->getCollection(TimeLimit::COLLECTION)->isEmpty()) {
$adapter = new TimeLimit("", 0, 1, $dbForConsole);
$adapter->setup();
}
/** @var array $collections */
$collections = Config::getParam('collections', []);
$consoleCollections = $collections['console'];
@@ -114,45 +205,21 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
if (($collection['$collection'] ?? '') !== Database::METADATA) {
continue;
}
if (!$dbForConsole->getCollection($key)->isEmpty()) {
if (!$dbForPlatform->getCollection($key)->isEmpty()) {
continue;
}
Console::success('[Setup] - Creating collection: ' . $collection['$id'] . '...');
Console::success('[Setup] - Creating console collection: ' . $collection['$id'] . '...');
$attributes = [];
$indexes = [];
$attributes = \array_map(fn ($attribute) => new Document($attribute), $collection['attributes']);
$indexes = \array_map(fn (array $index) => new Document($index), $collection['indexes']);
foreach ($collection['attributes'] as $attribute) {
$attributes[] = new Document([
'$id' => ID::custom($attribute['$id']),
'type' => $attribute['type'],
'size' => $attribute['size'],
'required' => $attribute['required'],
'signed' => $attribute['signed'],
'array' => $attribute['array'],
'filters' => $attribute['filters'],
'default' => $attribute['default'] ?? null,
'format' => $attribute['format'] ?? ''
]);
}
foreach ($collection['indexes'] as $index) {
$indexes[] = new Document([
'$id' => ID::custom($index['$id']),
'type' => $index['type'],
'attributes' => $index['attributes'],
'lengths' => $index['lengths'],
'orders' => $index['orders'],
]);
}
$dbForConsole->createCollection($key, $attributes, $indexes);
$dbForPlatform->createCollection($key, $attributes, $indexes);
}
if ($dbForConsole->getDocument('buckets', 'default')->isEmpty() && !$dbForConsole->exists($dbForConsole->getDatabase(), 'bucket_1')) {
if ($dbForPlatform->getDocument('buckets', 'default')->isEmpty() && !$dbForPlatform->exists($dbForPlatform->getDatabase(), 'bucket_1')) {
Console::success('[Setup] - Creating default bucket...');
$dbForConsole->createDocument('buckets', new Document([
$dbForPlatform->createDocument('buckets', new Document([
'$id' => ID::custom('default'),
'$collection' => ID::custom('buckets'),
'name' => 'Default',
@@ -172,7 +239,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
'search' => 'buckets Default',
]));
$bucket = $dbForConsole->getDocument('buckets', 'default');
$bucket = $dbForPlatform->getDocument('buckets', 'default');
Console::success('[Setup] - Creating files collection for default bucket...');
$files = $collections['buckets']['files'] ?? [];
@@ -180,34 +247,53 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
throw new Exception('Files collection is not configured.');
}
$attributes = [];
$indexes = [];
$attributes = \array_map(fn ($attribute) => new Document($attribute), $files['attributes']);
$indexes = \array_map(fn (array $index) => new Document($index), $files['indexes']);
foreach ($files['attributes'] as $attribute) {
$attributes[] = new Document([
'$id' => ID::custom($attribute['$id']),
'type' => $attribute['type'],
'size' => $attribute['size'],
'required' => $attribute['required'],
'signed' => $attribute['signed'],
'array' => $attribute['array'],
'filters' => $attribute['filters'],
'default' => $attribute['default'] ?? null,
'format' => $attribute['format'] ?? ''
]);
$dbForPlatform->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes);
}
$projectCollections = $collections['projects'];
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
$sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', ''));
$sharedTablesV2 = \array_diff($sharedTables, $sharedTablesV1);
$cache = $app->getResource('cache');
foreach ($sharedTablesV2 as $hostname) {
$adapter = $pools
->get($hostname)
->pop()
->getResource();
$dbForProject = (new Database($adapter, $cache))
->setDatabase('appwrite')
->setSharedTables(true)
->setTenant(null)
->setNamespace(System::getEnv('_APP_DATABASE_SHARED_NAMESPACE', ''));
try {
Console::success('[Setup] - Creating project database: ' . $hostname . '...');
$dbForProject->create();
} catch (Duplicate) {
Console::success('[Setup] - Skip: metadata table already exists');
}
foreach ($files['indexes'] as $index) {
$indexes[] = new Document([
'$id' => ID::custom($index['$id']),
'type' => $index['type'],
'attributes' => $index['attributes'],
'lengths' => $index['lengths'],
'orders' => $index['orders'],
]);
}
foreach ($projectCollections as $key => $collection) {
if (($collection['$collection'] ?? '') !== Database::METADATA) {
continue;
}
if (!$dbForProject->getCollection($key)->isEmpty()) {
continue;
}
$dbForConsole->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes);
$attributes = \array_map(fn ($attribute) => new Document($attribute), $collection['attributes']);
$indexes = \array_map(fn (array $index) => new Document($index), $collection['indexes']);
Console::success('[Setup] - Creating project collection: ' . $collection['$id'] . '...');
$dbForProject->createCollection($key, $attributes, $indexes);
}
}
$pools->reclaim();
@@ -218,6 +304,9 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
Console::success('Server started successfully (max payload is ' . number_format($payloadSize) . ' bytes)');
Console::info("Master pid {$http->master_pid}, manager pid {$http->manager_pid}");
// Start the task that starts fetching custom domains
$http->task([], 0);
// listen ctrl + c
Process::signal(2, function () use ($http) {
Console::log('Stop by Ctrl+C');
@@ -225,7 +314,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
});
});
$http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register) {
$http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register) {
App::setResource('swooleRequest', fn () => $swooleRequest);
App::setResource('swooleResponse', fn () => $swooleResponse);
@@ -274,10 +363,12 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
if (isset($user) && !$user->isEmpty()) {
$log->setUser(new User($user->getId()));
} else {
$log->setUser(new User('guest-' . hash('sha256', $request->getIP())));
}
$log->setNamespace("http");
$log->setServer(\gethostname());
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($th->getMessage());
@@ -297,6 +388,7 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
$action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD");
$log->setAction($action);
$log->addTag('service', $action);
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
@@ -335,4 +427,59 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
}
});
// Fetch domains every `DOMAIN_SYNC_TIMER` seconds and update in the memory
$http->on('Task', function () use ($register, $domains) {
$lastSyncUpdate = null;
$pools = $register->get('pools');
App::setResource('pools', fn () => $pools);
$app = new App('UTC');
/** @var Utopia\Database\Database $dbForPlatform */
$dbForPlatform = $app->getResource('dbForPlatform');
Console::loop(function () use ($dbForPlatform, $domains, &$lastSyncUpdate) {
try {
$time = DateTime::now();
$limit = 1000;
$sum = $limit;
$latestDocument = null;
while ($sum === $limit) {
$queries = [Query::limit($limit)];
if ($latestDocument !== null) {
$queries[] = Query::cursorAfter($latestDocument);
}
if ($lastSyncUpdate != null) {
$queries[] = Query::greaterThanEqual('$updatedAt', $lastSyncUpdate);
}
$queries[] = Query::equal('resourceType', ['function']);
$results = [];
try {
$results = Authorization::skip(fn () => $dbForPlatform->find('rules', $queries));
} catch (Throwable $th) {
Console::error($th->getMessage());
}
$sum = count($results);
foreach ($results as $document) {
$domain = $document->getAttribute('domain');
if (str_ends_with($domain, System::getEnv('_APP_DOMAIN_FUNCTIONS'))) {
continue;
}
$domains->set(md5($domain), ['value' => 1]);
}
$latestDocument = !empty(array_key_last($results)) ? $results[array_key_last($results)] : null;
}
$lastSyncUpdate = $time;
if ($sum > 0) {
Console::log("Sync domains tick: {$sum} domains were updated");
}
} catch (Throwable $th) {
Console::error($th->getMessage());
}
}, DOMAIN_SYNC_TIMER, 0, function ($error) {
Console::error($error);
});
});
$http->start();
+82 -41
View File
@@ -48,6 +48,7 @@ use Appwrite\Utopia\Request;
use MaxMind\Db\Reader;
use PHPMailer\PHPMailer\PHPMailer;
use Swoole\Database\PDOProxy;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\App;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Cache\Adapter\Sharding;
@@ -123,7 +124,7 @@ const APP_USER_ACCESS = 24 * 60 * 60; // 24 hours
const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 4318;
const APP_VERSION_STABLE = '1.6.0';
const APP_VERSION_STABLE = '1.6.1';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
@@ -231,6 +232,11 @@ const API_KEY_DYNAMIC = 'dynamic';
// Usage metrics
const METRIC_TEAMS = 'teams';
const METRIC_USERS = 'users';
const METRIC_WEBHOOKS_SENT = 'webhooks.events.sent';
const METRIC_WEBHOOKS_FAILED = 'webhooks.events.failed';
const METRIC_WEBHOOK_ID_SENT = '{webhookInternalId}.webhooks.events.sent';
const METRIC_WEBHOOK_ID_FAILED = '{webhookInternalId}.webhooks.events.failed';
const METRIC_AUTH_METHOD_PHONE = 'auth.method.phone';
const METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE = METRIC_AUTH_METHOD_PHONE . '.{countryCode}';
@@ -253,9 +259,15 @@ const METRIC_DOCUMENTS = 'documents';
const METRIC_DATABASE_ID_DOCUMENTS = '{databaseInternalId}.documents';
const METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS = '{databaseInternalId}.{collectionInternalId}.documents';
const METRIC_DATABASE_ID_COLLECTION_ID_STORAGE = '{databaseInternalId}.{collectionInternalId}.databases.storage';
const METRIC_DATABASES_OPERATIONS_READS = 'databases.operations.reads';
const METRIC_DATABASE_ID_OPERATIONS_READS = '{databaseInternalId}.databases.operations.reads';
const METRIC_DATABASES_OPERATIONS_WRITES = 'databases.operations.writes';
const METRIC_DATABASE_ID_OPERATIONS_WRITES = '{databaseInternalId}.databases.operations.writes';
const METRIC_BUCKETS = 'buckets';
const METRIC_FILES = 'files';
const METRIC_FILES_STORAGE = 'files.storage';
const METRIC_FILES_TRANSFORMATIONS = 'files.transformations';
const METRIC_BUCKET_ID_FILES_TRANSFORMATIONS = '{bucketInternalId}.files.transformations';
const METRIC_BUCKET_ID_FILES = '{bucketInternalId}.files';
const METRIC_BUCKET_ID_FILES_STORAGE = '{bucketInternalId}.files.storage';
const METRIC_FUNCTIONS = 'functions';
@@ -853,31 +865,31 @@ $register->set('pools', function () {
$connections = [
'console' => [
'type' => 'database',
'dsns' => System::getEnv('_APP_CONNECTIONS_DB_CONSOLE', $fallbackForDB),
'dsns' => $fallbackForDB,
'multiple' => false,
'schemes' => ['mariadb', 'mysql'],
],
'database' => [
'type' => 'database',
'dsns' => System::getEnv('_APP_CONNECTIONS_DB_PROJECT', $fallbackForDB),
'dsns' => $fallbackForDB,
'multiple' => true,
'schemes' => ['mariadb', 'mysql'],
],
'queue' => [
'type' => 'queue',
'dsns' => System::getEnv('_APP_CONNECTIONS_QUEUE', $fallbackForRedis),
'dsns' => $fallbackForRedis,
'multiple' => false,
'schemes' => ['redis'],
],
'pubsub' => [
'type' => 'pubsub',
'dsns' => System::getEnv('_APP_CONNECTIONS_PUBSUB', $fallbackForRedis),
'dsns' => $fallbackForRedis,
'multiple' => false,
'schemes' => ['redis'],
],
'cache' => [
'type' => 'cache',
'dsns' => System::getEnv('_APP_CONNECTIONS_CACHE', $fallbackForRedis),
'dsns' => $fallbackForRedis,
'multiple' => true,
'schemes' => ['redis'],
],
@@ -889,7 +901,7 @@ $register->set('pools', function () {
$multiprocessing = System::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled';
if ($multiprocessing) {
$workerCount = swoole_cpu_num() * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
} else {
$workerCount = 1;
}
@@ -949,12 +961,12 @@ $register->set('pools', function () {
});
},
'redis' => function () use ($dsnHost, $dsnPort, $dsnPass) {
$redis = new Redis();
$redis = new \Redis();
@$redis->pconnect($dsnHost, (int)$dsnPort);
if ($dsnPass) {
$redis->auth($dsnPass);
}
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
$redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
return $redis;
},
@@ -1213,12 +1225,12 @@ App::setResource('clients', function ($request, $console, $project) {
return \array_unique($clients);
}, ['request', 'console', 'project']);
App::setResource('user', function ($mode, $project, $console, $request, $response, $dbForProject, $dbForConsole) {
App::setResource('user', function ($mode, $project, $console, $request, $response, $dbForProject, $dbForPlatform) {
/** @var Appwrite\Utopia\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Database\Database $dbForConsole */
/** @var Utopia\Database\Database $dbForPlatform */
/** @var string $mode */
Authorization::setDefaultStatus(true);
@@ -1267,13 +1279,13 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
$user = new Document([]);
} else {
if ($project->getId() === 'console') {
$user = $dbForConsole->getDocument('users', Auth::$unique);
$user = $dbForPlatform->getDocument('users', Auth::$unique);
} else {
$user = $dbForProject->getDocument('users', Auth::$unique);
}
}
} else {
$user = $dbForConsole->getDocument('users', Auth::$unique);
$user = $dbForPlatform->getDocument('users', Auth::$unique);
}
if (
@@ -1316,14 +1328,14 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
}
$dbForProject->setMetadata('user', $user->getId());
$dbForConsole->setMetadata('user', $user->getId());
$dbForPlatform->setMetadata('user', $user->getId());
return $user;
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForConsole']);
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform']);
App::setResource('project', function ($dbForConsole, $request, $console) {
App::setResource('project', function ($dbForPlatform, $request, $console) {
/** @var Appwrite\Utopia\Request $request */
/** @var Utopia\Database\Database $dbForConsole */
/** @var Utopia\Database\Database $dbForPlatform */
/** @var Utopia\Database\Document $console */
$projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', ''));
@@ -1332,10 +1344,10 @@ App::setResource('project', function ($dbForConsole, $request, $console) {
return $console;
}
$project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId));
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
return $project;
}, ['dbForConsole', 'request', 'console']);
}, ['dbForPlatform', 'request', 'console']);
App::setResource('session', function (Document $user) {
if ($user->isEmpty()) {
@@ -1400,9 +1412,9 @@ App::setResource('console', function () {
]);
}, []);
App::setResource('dbForProject', function (Group $pools, Database $dbForConsole, Cache $cache, Document $project) {
App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForConsole;
return $dbForPlatform;
}
try {
@@ -1425,14 +1437,9 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForConsole,
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
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'));
}
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -1445,9 +1452,9 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForConsole,
}
return $database;
}, ['pools', 'dbForConsole', 'cache', 'project']);
}, ['pools', 'dbForPlatform', 'cache', 'project']);
App::setResource('dbForConsole', function (Group $pools, Cache $cache) {
App::setResource('dbForPlatform', function (Group $pools, Cache $cache) {
$dbAdapter = $pools
->get('console')
->pop()
@@ -1465,12 +1472,12 @@ App::setResource('dbForConsole', function (Group $pools, Cache $cache) {
return $database;
}, ['pools', 'cache']);
App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) {
App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) {
return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForConsole;
return $dbForPlatform;
}
try {
@@ -1487,7 +1494,9 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole,
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -1517,7 +1526,7 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole,
return $database;
};
}, ['pools', 'dbForConsole', 'cache']);
}, ['pools', 'dbForPlatform', 'cache']);
App::setResource('cache', function (Group $pools) {
$list = Config::getParam('pools-cache', []);
@@ -1534,6 +1543,27 @@ App::setResource('cache', function (Group $pools) {
return new Cache(new Sharding($adapters));
}, ['pools']);
App::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;
});
App::setResource('timelimit', function (\Redis $redis) {
return function (string $key, int $limit, int $time) use ($redis) {
return new TimeLimitRedis($key, $limit, $time, $redis);
};
}, ['redis']);
App::setResource('deviceForLocal', function () {
return new Local();
});
@@ -1799,7 +1829,7 @@ App::setResource('plan', function (array $plan = []) {
return [];
});
App::setResource('team', function (Document $project, Database $dbForConsole, App $utopia, Request $request) {
App::setResource('team', function (Document $project, Database $dbForPlatform, App $utopia, Request $request) {
$teamInternalId = '';
if ($project->getId() !== 'console') {
$teamInternalId = $project->getAttribute('teamInternalId', '');
@@ -1809,25 +1839,36 @@ App::setResource('team', function (Document $project, Database $dbForConsole, Ap
if (str_starts_with($path, '/v1/projects/:projectId')) {
$uri = $request->getURI();
$pid = explode('/', $uri)[3];
$p = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $pid));
$p = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $pid));
$teamInternalId = $p->getAttribute('teamInternalId', '');
} elseif ($path === '/v1/projects') {
$teamId = $request->getParam('teamId', '');
$team = Authorization::skip(fn () => $dbForConsole->getDocument('teams', $teamId));
$team = Authorization::skip(fn () => $dbForPlatform->getDocument('teams', $teamId));
return $team;
}
}
$team = Authorization::skip(function () use ($dbForConsole, $teamInternalId) {
return $dbForConsole->findOne('teams', [
$team = Authorization::skip(function () use ($dbForPlatform, $teamInternalId) {
return $dbForPlatform->findOne('teams', [
Query::equal('$internalId', [$teamInternalId]),
]);
});
return $team;
}, ['project', 'dbForConsole', 'utopia', 'request']);
}, ['project', 'dbForPlatform', 'utopia', 'request']);
App::setResource(
'isResourceBlocked',
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
);
App::setResource('previewHostname', function (Request $request) {
if (App::isDevelopment()) {
$host = $request->getQuery('appwrite-hostname') ?? '';
if (!empty($host)) {
return $host;
}
}
return '';
}, ['request']);
+63 -13
View File
@@ -13,7 +13,7 @@ use Swoole\Runtime;
use Swoole\Table;
use Swoole\Timer;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\Database\TimeLimit;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\App;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
@@ -29,6 +29,7 @@ use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\System\System;
use Utopia\Telemetry\Adapter\None as NoTelemetry;
use Utopia\WebSocket\Adapter;
use Utopia\WebSocket\Server;
@@ -92,7 +93,9 @@ if (!function_exists('getProjectDB')) {
$database = new Database($adapter, getCache());
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -135,6 +138,32 @@ if (!function_exists('getCache')) {
}
}
// Allows overriding
if (!function_exists('getRedis')) {
function getRedis(): \Redis
{
$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;
}
}
if (!function_exists('getTimelimit')) {
function getTimelimit(): TimeLimitRedis
{
return new TimeLimitRedis("", 0, 1, getRedis());
}
}
if (!function_exists('getRealtime')) {
function getRealtime(): Realtime
{
@@ -142,6 +171,13 @@ if (!function_exists('getRealtime')) {
}
}
if (!function_exists('getTelemetry')) {
function getTelemetry(int $workerId): Utopia\Telemetry\Adapter
{
return new NoTelemetry();
}
}
$realtime = getRealtime();
/**
@@ -157,7 +193,7 @@ $stats->create();
$containerId = uniqid();
$statsDocument = null;
$workerNumber = swoole_cpu_num() * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$workerNumber = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$adapter = new Adapter\Swoole(port: System::getEnv('PORT', 80));
$adapter
@@ -174,7 +210,7 @@ $logError = function (Throwable $error, string $action) use ($register) {
$log = new Log();
$log->setNamespace("realtime");
$log->setServer(gethostname());
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
@@ -274,6 +310,12 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime, $logError) {
Console::success('Worker ' . $workerId . ' started successfully');
$telemetry = getTelemetry($workerId);
$register->set('telemetry', fn () => $telemetry);
$register->set('telemetry.connectionCounter', fn () => $telemetry->createUpDownCounter('realtime.server.open_connections'));
$register->set('telemetry.connectionCreatedCounter', fn () => $telemetry->createCounter('realtime.server.connection.created'));
$register->set('telemetry.messageSentCounter', fn () => $telemetry->createCounter('realtime.server.message.sent'));
$attempts = 0;
$start = time();
@@ -416,6 +458,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
);
if (($num = count($receivers)) > 0) {
$register->get('telemetry.messageSentCounter')->add($num);
$stats->incr($event['project'], 'messages', $num);
}
});
@@ -464,7 +507,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
$dbForProject = getProjectDB($project);
$timelimit = $app->getResource('timelimit');
$console = $app->getResource('console'); /** @var Document $console */
$user = $app->getResource('user'); /** @var Document $user */
@@ -473,12 +516,12 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
*
* Abuse limits are connecting 128 times per minute and ip address.
*/
$timeLimit = new TimeLimit('url:{url},ip:{ip}', 128, 60, $dbForProject);
$timeLimit
$timelimit = $timelimit('url:{url},ip:{ip}', 128, 60);
$timelimit
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getURI());
$abuse = new Abuse($timeLimit);
$abuse = new Abuse($timelimit);
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled' && $abuse->check()) {
throw new Exception(Exception::REALTIME_TOO_MANY_MESSAGES, 'Too many requests');
@@ -519,6 +562,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
]
]));
$register->get('telemetry.connectionCounter')->add(1);
$register->get('telemetry.connectionCreatedCounter')->add(1);
$stats->set($project->getId(), [
'projectId' => $project->getId(),
'teamId' => $project->getAttribute('teamId')
@@ -573,7 +619,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
*
* Abuse limits are sending 32 times per minute and connection.
*/
$timeLimit = new TimeLimit('url:{url},connection:{connection}', 32, 60, $database);
$timeLimit = getTimelimit('url:{url},connection:{connection}', 32, 60);
$timeLimit
->setParam('{connection}', $connection)
@@ -592,9 +638,12 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
}
switch ($message['type']) {
/**
* This type is used to authenticate.
*/
case 'ping':
$server->send([$connection], json_encode([
'type' => 'pong'
]));
break;
case 'authentication':
if (!array_key_exists('session', $message['data'])) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
@@ -652,9 +701,10 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
}
});
$server->onClose(function (int $connection) use ($realtime, $stats) {
$server->onClose(function (int $connection) use ($realtime, $stats, $register) {
if (array_key_exists($connection, $realtime->connections)) {
$stats->decr($realtime->connections[$connection]['projectId'], 'connectionsTotal');
$register->get('telemetry.connectionCounter')->add(-1);
}
$realtime->unsubscribe($connection);
+1 -1
View File
@@ -167,7 +167,7 @@ $image = $this->getParam('image', '');
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: <?php echo $organization; ?>/console:5.0.12
image: <?php echo $organization; ?>/console:5.2.27
restart: unless-stopped
networks:
- appwrite
+98 -17
View File
@@ -2,6 +2,7 @@
require_once __DIR__ . '/init.php';
use Appwrite\Certificates\LetsEncrypt;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
@@ -16,7 +17,7 @@ use Appwrite\Event\Usage;
use Appwrite\Event\UsageDump;
use Appwrite\Platform\Appwrite;
use Swoole\Runtime;
use Utopia\App;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
@@ -41,7 +42,7 @@ Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
Server::setResource('register', fn () => $register);
Server::setResource('dbForConsole', function (Cache $cache, Registry $register) {
Server::setResource('dbForPlatform', function (Cache $cache, Registry $register) {
$pools = $register->get('pools');
$database = $pools
->get('console')
@@ -54,7 +55,7 @@ Server::setResource('dbForConsole', function (Cache $cache, Registry $register)
return $adapter;
}, ['cache', 'register']);
Server::setResource('project', function (Message $message, Database $dbForConsole) {
Server::setResource('project', function (Message $message, Database $dbForPlatform) {
$payload = $message->getPayload() ?? [];
$project = new Document($payload['project'] ?? []);
@@ -62,12 +63,12 @@ Server::setResource('project', function (Message $message, Database $dbForConsol
return $project;
}
return $dbForConsole->getDocument('projects', $project->getId());
}, ['message', 'dbForConsole']);
return $dbForPlatform->getDocument('projects', $project->getId());
}, ['message', 'dbForPlatform']);
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForConsole) {
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForPlatform) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForConsole;
return $dbForPlatform;
}
$pools = $register->get('pools');
@@ -93,7 +94,9 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -106,14 +109,14 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
}
return $database;
}, ['cache', 'register', 'message', 'project', 'dbForConsole']);
}, ['cache', 'register', 'message', 'project', 'dbForPlatform']);
Server::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) {
Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases): Database {
return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases): Database {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForConsole;
return $dbForPlatform;
}
try {
@@ -126,7 +129,9 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForConso
if (isset($databases[$dsn->getHost()])) {
$database = $databases[$dsn->getHost()];
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -150,7 +155,9 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForConso
$databases[$dsn->getHost()] = $database;
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -164,10 +171,10 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForConso
return $database;
};
}, ['pools', 'dbForConsole', 'cache']);
}, ['pools', 'dbForPlatform', 'cache']);
Server::setResource('abuseRetention', function () {
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400));
return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400);
});
Server::setResource('auditRetention', function () {
@@ -194,6 +201,27 @@ Server::setResource('cache', function (Registry $register) {
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('queueForUsage', function (Connection $queue) {
@@ -277,6 +305,59 @@ Server::setResource(
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
);
Server::setResource('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) 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());
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());
};
}, ['register', 'project']);
$pools = $register->get('pools');
$platform = new Appwrite();
@@ -335,7 +416,7 @@ $worker
if ($logger) {
$log->setNamespace("appwrite-worker");
$log->setServer(\gethostname());
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
+9 -4
View File
@@ -45,13 +45,13 @@
"ext-sockets": "*",
"appwrite/php-runtimes": "0.16.*",
"appwrite/php-clamav": "2.0.*",
"utopia-php/abuse": "0.43.0",
"utopia-php/abuse": "0.46.*",
"utopia-php/analytics": "0.10.*",
"utopia-php/audit": "0.43.0",
"utopia-php/audit": "0.43.*",
"utopia-php/cache": "0.11.*",
"utopia-php/cli": "0.15.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.53.16",
"utopia-php/database": "0.53.200",
"utopia-php/domains": "0.5.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/framework": "0.33.*",
@@ -70,6 +70,7 @@
"utopia-php/storage": "0.18.*",
"utopia-php/swoole": "0.8.*",
"utopia-php/system": "0.9.*",
"utopia-php/telemetry": "0.1.*",
"utopia-php/vcs": "0.8.*",
"utopia-php/websocket": "0.1.*",
"matomo/device-detector": "6.1.*",
@@ -83,7 +84,7 @@
},
"require-dev": {
"ext-fileinfo": "*",
"appwrite/sdk-generator": "0.39.*",
"appwrite/sdk-generator": "0.39.28",
"phpunit/phpunit": "9.5.20",
"swoole/ide-helper": "5.1.2",
"textalk/websocket": "1.5.7",
@@ -96,6 +97,10 @@
"config": {
"platform": {
"php": "8.3"
},
"allow-plugins": {
"php-http/discovery": false,
"tbachert/spi": false
}
}
}
Generated
+1952 -448
View File
File diff suppressed because it is too large Load Diff
+7 -2
View File
@@ -193,11 +193,14 @@ services:
- _APP_EXPERIMENT_LOGGING_PROVIDER
- _APP_EXPERIMENT_LOGGING_CONFIG
- _APP_DATABASE_SHARED_TABLES
- _APP_DATABASE_SHARED_TABLES_V1
- _APP_DATABASE_SHARED_NAMESPACE
- _APP_FUNCTIONS_CREATION_ABUSE_LIMIT
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: appwrite/console:5.0.12
image: appwrite/console:5.2.27
restart: unless-stopped
networks:
- appwrite
@@ -382,6 +385,8 @@ services:
- _APP_EXECUTOR_SECRET
- _APP_EXECUTOR_HOST
- _APP_DATABASE_SHARED_TABLES
- _APP_DATABASE_SHARED_TABLES_V1
- _APP_EMAIL_CERTIFICATES
appwrite-worker-databases:
entrypoint: worker-databases
@@ -864,7 +869,7 @@ services:
appwrite-assistant:
container_name: appwrite-assistant
image: appwrite/assistant:0.5.0
image: appwrite/assistant:0.7.0
networks:
- appwrite
environment:
@@ -12,5 +12,6 @@ mutation {
providerId
providerType
identifier
expired
}
}
@@ -33,6 +33,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -28,6 +28,7 @@ query {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -31,6 +31,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -30,6 +30,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -31,6 +31,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -30,6 +30,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -31,6 +31,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -31,6 +31,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -30,6 +30,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -11,5 +11,6 @@ mutation {
providerId
providerType
identifier
expired
}
}
@@ -28,6 +28,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -17,6 +17,7 @@ mutation {
providerId
providerType
identifier
expired
}
userId
userName
@@ -1,3 +1,3 @@
Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST /v1/account/sessions/token](https://appwrite.io/docs/references/cloud/client-web/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.
Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST /v1/account/sessions/token](https://appwrite.io/docs/references/cloud/client-web/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.
A user is limited to 10 active sessions at a time by default. [Learn more about session limits](https://appwrite.io/docs/authentication-security#limits).
-389
View File
@@ -1,389 +0,0 @@
<?php
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
class Firebase extends OAuth2
{
/**
* @var array
*/
protected array $user = [];
/**
* @var array
*/
protected array $tokens = [];
/**
* @var array
*/
protected array $scopes = [
'https://www.googleapis.com/auth/firebase',
'https://www.googleapis.com/auth/datastore',
'https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/identitytoolkit',
'https://www.googleapis.com/auth/userinfo.profile'
];
/**
* @var array
*/
protected array $iamPermissions = [
// Database
'datastore.databases.get',
'datastore.databases.list',
'datastore.entities.get',
'datastore.entities.list',
'datastore.indexes.get',
'datastore.indexes.list',
// Generic Firebase permissions
'firebase.projects.get',
// Auth
'firebaseauth.configs.get',
'firebaseauth.configs.getHashConfig',
'firebaseauth.configs.getSecret',
'firebaseauth.users.get',
'identitytoolkit.tenants.get',
'identitytoolkit.tenants.list',
// IAM Assignment
'iam.serviceAccounts.list',
// Storage
'storage.buckets.get',
'storage.buckets.list',
'storage.objects.get',
'storage.objects.list'
];
/**
* @return string
*/
public function getName(): string
{
return 'firebase';
}
/**
* @return string
*/
public function getLoginURL(): string
{
return 'https://accounts.google.com/o/oauth2/v2/auth?' . \http_build_query([
'access_type' => 'offline',
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'scope' => \implode(' ', $this->getScopes()),
'state' => \json_encode($this->state),
'response_type' => 'code',
'prompt' => 'consent',
]);
}
/**
* @param string $code
*
* @return array
*/
protected function getTokens(string $code): array
{
if (empty($this->tokens)) {
$response = $this->request(
'POST',
'https://oauth2.googleapis.com/token',
[],
\http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'client_secret' => $this->appSecret,
'code' => $code,
'grant_type' => 'authorization_code'
])
);
$this->tokens = \json_decode($response, true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken): array
{
$response = $this->request(
'POST',
'https://oauth2.googleapis.com/token',
[],
\http_build_query([
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken
])
);
$output = [];
\parse_str($response, $output);
$this->tokens = $output;
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['id'] ?? '';
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserEmail(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['email'] ?? '';
}
/**
* Check if the OAuth email is verified
*
* @link https://docs.github.com/en/rest/users/emails#list-email-addresses-for-the-authenticated-user
*
* @param string $accessToken
*
* @return bool
*/
public function isEmailVerified(string $accessToken): bool
{
$user = $this->getUser($accessToken);
if ($user['verified'] ?? false) {
return true;
}
return false;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserName(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['name'] ?? '';
}
/**
* @param string $accessToken
*
* @return array
*/
protected function getUser(string $accessToken)
{
if (empty($this->user)) {
$response = $this->request(
'GET',
'https://www.googleapis.com/oauth2/v1/userinfo?access_token=' . \urlencode($accessToken),
[],
);
$this->user = \json_decode($response, true);
}
return $this->user;
}
public function getProjects(string $accessToken): array
{
$projects = $this->request('GET', 'https://firebase.googleapis.com/v1beta1/projects', ['Authorization: Bearer ' . \urlencode($accessToken)]);
$projects = \json_decode($projects, true);
return $projects['results'];
}
/*
Be careful with the setIAMPolicy method, it will overwrite all existing policies
**/
public function assignIAMRole(string $accessToken, string $email, string $projectId, array $role)
{
// Get IAM Roles
$iamRoles = $this->request('POST', 'https://cloudresourcemanager.googleapis.com/v1/projects/' . $projectId . ':getIamPolicy', [
'Authorization: Bearer ' . \urlencode($accessToken),
'Content-Type: application/json'
]);
$iamRoles = \json_decode($iamRoles, true);
$iamRoles['bindings'][] = [
'role' => $role['name'],
'members' => [
'serviceAccount:' . $email
]
];
// Set IAM Roles
$this->request('POST', 'https://cloudresourcemanager.googleapis.com/v1/projects/' . $projectId . ':setIamPolicy', [
'Authorization: Bearer ' . \urlencode($accessToken),
'Content-Type: application/json'
], \json_encode([
'policy' => $iamRoles
]));
}
private function generateRandomString($length = 10): string
{
$characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[random_int(0, $charactersLength - 1)];
}
return $randomString;
}
private function createCustomRole(string $accessToken, string $projectId): array
{
// Check if role already exists
try {
$role = $this->request('GET', 'https://iam.googleapis.com/v1/projects/' . $projectId . '/roles/appwriteMigrations', [
'Content-Type: application/json',
'Authorization: Bearer ' . \urlencode($accessToken),
]);
$role = \json_decode($role, true);
return $role;
} catch (\Throwable $e) {
if ($e->getCode() !== 404) {
throw $e;
}
}
// Create role if doesn't exist or isn't correct
$role = $this->request(
'POST',
'https://iam.googleapis.com/v1/projects/' . $projectId . '/roles/',
[
'Content-Type: application/json',
'Authorization: Bearer ' . \urlencode($accessToken),
],
\json_encode(
[
'roleId' => 'appwriteMigrations',
'role' => [
'title' => 'Appwrite Migrations',
'description' => 'A helper role for Appwrite Migrations',
'includedPermissions' => $this->iamPermissions,
'stage' => 'GA'
]
]
)
);
return json_decode($role, true);
}
public function createServiceAccount(string $accessToken, string $projectId): array
{
// Create Service Account
$uid = $this->generateRandomString();
$response = $this->request(
'POST',
'https://iam.googleapis.com/v1/projects/' . $projectId . '/serviceAccounts',
[
'Authorization: Bearer ' . \urlencode($accessToken),
'Content-Type: application/json'
],
\json_encode([
'accountId' => 'appwrite-' . $uid,
'serviceAccount' => [
'displayName' => 'Appwrite Migrations ' . $uid
]
])
);
$response = json_decode($response, true);
// Create and assign IAM Roles
$role = $this->createCustomRole($accessToken, $projectId);
\sleep(1); // Wait for IAM to propagate changes.
$this->assignIAMRole($accessToken, $response['email'], $projectId, $role);
// Create Service Account Key
$responseKey = $this->request(
'POST',
'https://iam.googleapis.com/v1/projects/' . $projectId . '/serviceAccounts/' . $response['email'] . '/keys',
[
'Authorization: Bearer ' . \urlencode($accessToken),
'Content-Type: application/json'
]
);
$responseKey = json_decode($responseKey, true);
return json_decode(base64_decode($responseKey['privateKeyData']), true);
}
public function cleanupServiceAccounts(string $accessToken, string $projectId)
{
// List Service Accounts
$response = $this->request(
'GET',
'https://iam.googleapis.com/v1/projects/' . $projectId . '/serviceAccounts',
[
'Authorization: Bearer ' . \urlencode($accessToken),
'Content-Type: application/json'
]
);
$response = json_decode($response, true);
if (empty($response['accounts'])) {
return false;
}
foreach ($response['accounts'] as $account) {
if (strpos($account['email'], 'appwrite-') !== false) {
$this->request(
'DELETE',
'https://iam.googleapis.com/v1/projects/' . $projectId . '/serviceAccounts/' . $account['email'],
[
'Authorization: Bearer ' . \urlencode($accessToken),
'Content-Type: application/json'
]
);
}
}
return true;
}
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace Appwrite\Certificates;
use Utopia\Logger\Log;
interface Adapter
{
public function issueCertificate(string $certName, string $domain): ?string;
public function isRenewRequired(string $domain, Log $log): bool;
public function deleteCertificate(string $domain): void;
}
+125
View File
@@ -0,0 +1,125 @@
<?php
namespace Appwrite\Certificates;
use Exception;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\DateTime;
use Utopia\Logger\Log;
class LetsEncrypt implements Adapter
{
private string $email;
public function __construct(string $email)
{
$this->email = $email;
}
public function issueCertificate(string $certName, string $domain): ?string
{
$stdout = '';
$stderr = '';
$staging = (App::isProduction()) ? '' : ' --dry-run';
$exit = Console::execute(
"certbot certonly -v --webroot --noninteractive --agree-tos{$staging}"
. " --email " . $this->email
. " --cert-name " . $certName
. " -w " . APP_STORAGE_CERTIFICATES
. " -d {$domain}",
'',
$stdout,
$stderr
);
// Unexpected error, usually 5XX, API limits, ...
if ($exit !== 0) {
throw new Exception('Failed to issue a certificate with message: ' . $stderr);
}
// Prepare folder in storage for domain
$path = APP_STORAGE_CERTIFICATES . '/' . $domain;
if (!\is_readable($path)) {
if (!\mkdir($path, 0755, true)) {
throw new Exception('Failed to create path for certificate.');
}
}
// Move generated files
if (!@\rename('/etc/letsencrypt/live/' . $certName . '/cert.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem')) {
throw new Exception('Failed to rename certificate cert.pem. Let\'s Encrypt log: ' . $stderr . ' ; ' . $stdout);
}
if (!@\rename('/etc/letsencrypt/live/' . $certName . '/chain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/chain.pem')) {
throw new Exception('Failed to rename certificate chain.pem. Let\'s Encrypt log: ' . $stderr . ' ; ' . $stdout);
}
if (!@\rename('/etc/letsencrypt/live/' . $certName . '/fullchain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/fullchain.pem')) {
throw new Exception('Failed to rename certificate fullchain.pem. Let\'s Encrypt log: ' . $stderr . ' ; ' . $stdout);
}
if (!@\rename('/etc/letsencrypt/live/' . $certName . '/privkey.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/privkey.pem')) {
throw new Exception('Failed to rename certificate privkey.pem. Let\'s Encrypt log: ' . $stderr . ' ; ' . $stdout);
}
$config = \implode(PHP_EOL, [
"tls:",
" certificates:",
" - certFile: /storage/certificates/{$domain}/fullchain.pem",
" keyFile: /storage/certificates/{$domain}/privkey.pem"
]);
// Save configuration into Traefik using our new cert files
if (!\file_put_contents(APP_STORAGE_CONFIG . '/' . $domain . '.yml', $config)) {
throw new Exception('Failed to save Traefik configuration.');
}
$certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem';
$certData = openssl_x509_parse(file_get_contents($certPath));
$validTo = $certData['validTo_time_t'] ?? null;
$dt = (new \DateTime())->setTimestamp($validTo);
return DateTime::addSeconds($dt, -60 * 60 * 24 * 30);
}
public function isRenewRequired(string $domain, Log $log): bool
{
$certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem';
if (\file_exists($certPath)) {
$certData = openssl_x509_parse(file_get_contents($certPath));
$validTo = $certData['validTo_time_t'] ?? 0;
if (empty($validTo)) {
$log->addTag('certificateDomain', $domain);
throw new Exception('Unable to read certificate file (cert.pem).');
}
// LetsEncrypt allows renewal 30 days before expiry
$expiryInAdvance = (60 * 60 * 24 * 30);
if ($validTo - $expiryInAdvance > \time()) {
$log->addTag('certificateDomain', $domain);
$log->addExtra('certificateData', \is_array($certData) ? \json_encode($certData) : \strval($certData));
return false;
}
}
return true;
}
public function deleteCertificate(string $domain): void
{
$directory = APP_STORAGE_CERTIFICATES . '/' . $domain;
$checkTraversal = realpath($directory) === $directory;
if ($checkTraversal && is_dir($directory)) {
// Delete files, so Traefik is aware of change
array_map('unlink', glob($directory . '/*.*'));
rmdir($directory);
Console::info("Deleted certificate files for {$domain}");
} else {
Console::info("No certificate files found for {$domain}");
}
}
}
+7 -1
View File
@@ -42,6 +42,7 @@ class Usage extends Event
*/
public function addMetric(string $key, int $value): self
{
$this->metrics[] = [
'key' => $key,
'value' => $value,
@@ -62,10 +63,15 @@ class Usage extends Event
}
$client = new Client($this->queue, $this->connection);
return $client->enqueue([
$result = $client->enqueue([
'project' => $this->getProject(),
'reduce' => $this->reduce,
'metrics' => $this->metrics,
]);
$this->metrics = [];
return $result;
}
}
+13
View File
@@ -2,6 +2,7 @@
namespace Appwrite\Event;
use Utopia\Database\Document;
use Utopia\Queue\Connection;
class Webhook extends Event
@@ -14,4 +15,16 @@ class Webhook extends Event
->setQueue(Event::WEBHOOK_QUEUE_NAME)
->setClass(Event::WEBHOOK_CLASS_NAME);
}
public function trigger(): string|bool
{
/** Filter out context and trim project to keep the payload small */
$this->context = [];
$this->project = new Document([
'$id' => $this->project->getId(),
'$internalId' => $this->project->getInternalId(),
]);
return parent::trigger();
}
}
+1
View File
@@ -91,6 +91,7 @@ abstract class Migration
'1.5.10' => 'V20',
'1.5.11' => 'V20',
'1.6.0' => 'V21',
'1.6.1' => 'V21',
];
/**
+11 -18
View File
@@ -23,13 +23,13 @@ class Maintenance extends Action
{
$this
->desc('Schedules maintenance tasks and publishes them to our queues')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForCertificates')
->inject('queueForDeletes')
->callback(fn (Database $dbForConsole, Certificate $queueForCertificates, Delete $queueForDeletes) => $this->action($dbForConsole, $queueForCertificates, $queueForDeletes));
->callback(fn (Database $dbForPlatform, Certificate $queueForCertificates, Delete $queueForDeletes) => $this->action($dbForPlatform, $queueForCertificates, $queueForDeletes));
}
public function action(Database $dbForConsole, Certificate $queueForCertificates, Delete $queueForDeletes): void
public function action(Database $dbForPlatform, Certificate $queueForCertificates, Delete $queueForDeletes): void
{
Console::title('Maintenance V1');
Console::success(APP_NAME . ' maintenance process v1 has started');
@@ -41,19 +41,19 @@ class Maintenance extends Action
$cacheRetention = (int) System::getEnv('_APP_MAINTENANCE_RETENTION_CACHE', '2592000'); // 30 days
$schedulesDeletionRetention = (int) System::getEnv('_APP_MAINTENANCE_RETENTION_SCHEDULES', '86400'); // 1 Day
Console::loop(function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForConsole, $queueForDeletes, $queueForCertificates) {
Console::loop(function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $queueForDeletes, $queueForCertificates) {
$time = DateTime::now();
Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds");
$this->foreachProject($dbForConsole, function (Document $project) use ($queueForDeletes, $usageStatsRetentionHourly) {
$this->foreachProject($dbForPlatform, function (Document $project) use ($queueForDeletes, $usageStatsRetentionHourly) {
$queueForDeletes->setProject($project);
$this->notifyProjects($queueForDeletes, $usageStatsRetentionHourly);
});
$this->notifyDeleteConnections($queueForDeletes);
$this->renewCertificates($dbForConsole, $queueForCertificates);
$this->renewCertificates($dbForPlatform, $queueForCertificates);
$this->notifyDeleteCache($cacheRetention, $queueForDeletes);
$this->notifyDeleteSchedules($schedulesDeletionRetention, $queueForDeletes);
}, $interval, $delay);
@@ -66,13 +66,12 @@ class Maintenance extends Action
{
$this->notifyDeleteTargets($queueForDeletes);
$this->notifyDeleteExecutionLogs($queueForDeletes);
$this->notifyDeleteAbuseLogs($queueForDeletes);
$this->notifyDeleteAuditLogs($queueForDeletes);
$this->notifyDeleteUsageStats($usageStatsRetentionHourly, $queueForDeletes);
$this->notifyDeleteExpiredSessions($queueForDeletes);
}
protected function foreachProject(Database $dbForConsole, callable $callback): void
protected function foreachProject(Database $dbForPlatform, callable $callback): void
{
// TODO: @Meldiron name of this method no longer matches. It does not delete, and it gives whole document
$count = 0;
@@ -82,7 +81,7 @@ class Maintenance extends Action
$executionStart = \microtime(true);
while ($sum === $limit) {
$projects = $dbForConsole->find('projects', [Query::limit($limit), Query::offset($chunk * $limit)]);
$projects = $dbForPlatform->find('projects', [Query::limit($limit), Query::offset($chunk * $limit)]);
$chunk++;
@@ -106,13 +105,6 @@ class Maintenance extends Action
->trigger();
}
private function notifyDeleteAbuseLogs(Delete $queueForDeletes): void
{
$queueForDeletes
->setType(DELETE_TYPE_ABUSE)
->trigger();
}
private function notifyDeleteAuditLogs(Delete $queueForDeletes): void
{
$queueForDeletes
@@ -143,12 +135,13 @@ class Maintenance extends Action
->trigger();
}
private function renewCertificates(Database $dbForConsole, Certificate $queueForCertificate): void
private function renewCertificates(Database $dbForPlatform, Certificate $queueForCertificate): void
{
$time = DateTime::now();
$certificates = $dbForConsole->find('certificates', [
$certificates = $dbForPlatform->find('certificates', [
Query::lessThan('attempts', 5), // Maximum 5 attempts
Query::isNotNull('renewDate'),
Query::lessThanEqual('renewDate', $time), // includes 60 days cooldown (we have 30 days to renew)
Query::limit(200), // Limit 200 comes from LetsEncrypt (300 orders per 3 hours, keeping some for new domains)
]);
+10 -10
View File
@@ -30,12 +30,12 @@ class Migrate extends Action
->desc('Migrate Appwrite to new version')
/** @TODO APP_VERSION_STABLE needs to be defined */
->param('version', APP_VERSION_STABLE, new Text(8), 'Version to migrate to.', true)
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('register')
->callback(function ($version, $dbForConsole, $getProjectDB, Registry $register) {
\Co\run(function () use ($version, $dbForConsole, $getProjectDB, $register) {
$this->action($version, $dbForConsole, $getProjectDB, $register);
->callback(function ($version, $dbForPlatform, $getProjectDB, Registry $register) {
\Co\run(function () use ($version, $dbForPlatform, $getProjectDB, $register) {
$this->action($version, $dbForPlatform, $getProjectDB, $register);
});
});
}
@@ -58,7 +58,7 @@ class Migrate extends Action
}
}
public function action(string $version, Database $dbForConsole, callable $getProjectDB, Registry $register)
public function action(string $version, Database $dbForPlatform, callable $getProjectDB, Registry $register)
{
Authorization::disable();
if (!array_key_exists($version, Migration::$versions)) {
@@ -93,10 +93,10 @@ class Migrate extends Action
$count = 0;
try {
$totalProjects = $dbForConsole->count('projects') + 1;
$totalProjects = $dbForPlatform->count('projects') + 1;
} catch (\Throwable $th) {
$dbForConsole->setNamespace('_console');
$totalProjects = $dbForConsole->count('projects') + 1;
$dbForPlatform->setNamespace('_console');
$totalProjects = $dbForPlatform->count('projects') + 1;
}
$class = 'Appwrite\\Migration\\Version\\' . Migration::$versions[$version];
@@ -120,7 +120,7 @@ class Migrate extends Action
$projectDB = $getProjectDB($project);
$projectDB->disableValidation();
$migration
->setProject($project, $projectDB, $dbForConsole)
->setProject($project, $projectDB, $dbForPlatform)
->setPDO($register->get('db', true))
->execute();
} catch (\Throwable $th) {
@@ -132,7 +132,7 @@ class Migrate extends Action
}
$sum = \count($projects);
$projects = $dbForConsole->find('projects', [Query::limit($limit), Query::offset($offset)]);
$projects = $dbForPlatform->find('projects', [Query::limit($limit), Query::offset($offset)]);
$offset = $offset + $limit;
$count = $count + $sum;
+24 -9
View File
@@ -25,6 +25,9 @@ use Appwrite\Spec\Swagger2;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Platform\Action;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
class SDKs extends Action
{
@@ -37,23 +40,35 @@ class SDKs extends Action
{
$this
->desc('Generate Appwrite SDKs')
->callback(fn () => $this->action());
->param('platform', null, new Nullable(new Text(256)), 'Selected Platform', optional: true)
->param('sdk', null, new Nullable(new Text(256)), 'Selected SDK', optional:true)
->param('version', null, new Nullable(new Text(256)), 'Selected SDK', optional:true)
->param('git', null, new Nullable(new WhiteList(['yes', 'no'])), 'Should we use git push?', optional: true)
->param('production', null, new Nullable(new WhiteList(['yes', 'no'])), 'Should we push to production?', optional:true)
->param('message', null, new Nullable(new Text(256)), 'Commit Message', optional:true)
->callback([$this, 'action']);
}
public function action(): void
public function action(?string $selectedPlatform, ?string $selectedSDK, ?string $version, ?string $git, ?string $production, ?string $message)
{
$platforms = Config::getParam('platforms');
$selectedPlatform = Console::confirm('Choose Platform ("' . APP_PLATFORM_CLIENT . '", "' . APP_PLATFORM_SERVER . '", "' . APP_PLATFORM_CONSOLE . '" or "*" for all):');
$selectedSDK = \strtolower(Console::confirm('Choose SDK ("*" for all):'));
$version = Console::confirm('Choose an Appwrite version');
$git = (Console::confirm('Should we use git push? (yes/no)') == 'yes');
$production = ($git) ? (Console::confirm('Type "Appwrite" to push code to production git repos') == 'Appwrite') : false;
$message = ($git) ? Console::confirm('Please enter your commit message:') : '';
$selectedPlatform ??= Console::confirm('Choose Platform ("' . APP_PLATFORM_CLIENT . '", "' . APP_PLATFORM_SERVER . '", "' . APP_PLATFORM_CONSOLE . '" or "*" for all):');
$selectedSDK ??= \strtolower(Console::confirm('Choose SDK ("*" for all):'));
$version ??= Console::confirm('Choose an Appwrite version');
$git ??= Console::confirm('Should we use git push? (yes/no)');
$git = $git === 'yes';
if ($git) {
$production ??= Console::confirm('Type "Appwrite" to push code to production git repos');
$production = $production === 'Appwrite';
$message ??= Console::confirm('Please enter your commit message:');
}
if (!in_array($version, ['0.6.x', '0.7.x', '0.8.x', '0.9.x', '0.10.x', '0.11.x', '0.12.x', '0.13.x', '0.14.x', '0.15.x', '1.0.x', '1.1.x', '1.2.x', '1.3.x', '1.4.x', '1.5.x', '1.6.x', 'latest'])) {
throw new \Exception('Unknown version given');
}
$platforms = Config::getParam('platforms');
foreach ($platforms as $key => $platform) {
if ($selectedPlatform !== $key && $selectedPlatform !== '*') {
continue;
+24 -12
View File
@@ -9,6 +9,7 @@ use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Pools\Group;
use Utopia\System\System;
@@ -25,7 +26,7 @@ abstract class ScheduleBase extends Action
abstract public static function getName(): string;
abstract public static function getSupportedResource(): string;
abstract public static function getCollectionId(): string;
abstract protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void;
abstract protected function enqueueResources(Group $pools, Database $dbForPlatform, callable $getProjectDB): void;
public function __construct()
{
@@ -34,9 +35,20 @@ abstract class ScheduleBase extends Action
$this
->desc("Execute {$type}s scheduled in Appwrite")
->inject('pools')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->callback(fn (Group $pools, Database $dbForConsole, callable $getProjectDB) => $this->action($pools, $dbForConsole, $getProjectDB));
->callback(fn (Group $pools, Database $dbForPlatform, callable $getProjectDB) => $this->action($pools, $dbForPlatform, $getProjectDB));
}
protected function updateProjectAccess(Document $project, Database $dbForPlatform): void
{
if (!$project->isEmpty() && $project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$project->setAttribute('accessedAt', DateTime::now());
Authorization::skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
}
}
}
/**
@@ -44,7 +56,7 @@ abstract class ScheduleBase extends Action
* 2. Create timer that sync all changes from 'schedules' collection to local copy. Only reading changes thanks to 'resourceUpdatedAt' attribute
* 3. Create timer that prepares coroutines for soon-to-execute schedules. When it's ready, coroutine sleeps until exact time before sending request to worker.
*/
public function action(Group $pools, Database $dbForConsole, callable $getProjectDB): void
public function action(Group $pools, Database $dbForPlatform, callable $getProjectDB): void
{
Console::title(\ucfirst(static::getSupportedResource()) . ' scheduler V1');
Console::success(APP_NAME . ' ' . \ucfirst(static::getSupportedResource()) . ' scheduler v1 has started');
@@ -56,8 +68,8 @@ abstract class ScheduleBase extends Action
* @throws Exception
* @var Document $schedule
*/
$getSchedule = function (Document $schedule) use ($dbForConsole, $getProjectDB): array {
$project = $dbForConsole->getDocument('projects', $schedule->getAttribute('projectId'));
$getSchedule = function (Document $schedule) use ($dbForPlatform, $getProjectDB): array {
$project = $dbForPlatform->getDocument('projects', $schedule->getAttribute('projectId'));
$resource = $getProjectDB($project)->getDocument(
static::getCollectionId(),
@@ -91,7 +103,7 @@ abstract class ScheduleBase extends Action
$paginationQueries[] = Query::cursorAfter($latestDocument);
}
$results = $dbForConsole->find('schedules', \array_merge($paginationQueries, [
$results = $dbForPlatform->find('schedules', \array_merge($paginationQueries, [
Query::equal('region', [System::getEnv('_APP_REGION', 'default')]),
Query::equal('resourceType', [static::getSupportedResource()]),
Query::equal('active', [true]),
@@ -119,11 +131,11 @@ abstract class ScheduleBase extends Action
Console::success("Starting timers at " . DateTime::now());
run(function () use ($dbForConsole, &$lastSyncUpdate, $getSchedule, $pools, $getProjectDB) {
run(function () use ($dbForPlatform, &$lastSyncUpdate, $getSchedule, $pools, $getProjectDB) {
/**
* The timer synchronize $schedules copy with database collection.
*/
Timer::tick(static::UPDATE_TIMER * 1000, function () use ($dbForConsole, &$lastSyncUpdate, $getSchedule, $pools) {
Timer::tick(static::UPDATE_TIMER * 1000, function () use ($dbForPlatform, &$lastSyncUpdate, $getSchedule, $pools) {
$time = DateTime::now();
$timerStart = \microtime(true);
@@ -141,7 +153,7 @@ abstract class ScheduleBase extends Action
$paginationQueries[] = Query::cursorAfter($latestDocument);
}
$results = $dbForConsole->find('schedules', \array_merge($paginationQueries, [
$results = $dbForPlatform->find('schedules', \array_merge($paginationQueries, [
Query::equal('region', [System::getEnv('_APP_REGION', 'default')]),
Query::equal('resourceType', [static::getSupportedResource()]),
Query::greaterThanEqual('resourceUpdatedAt', $lastSyncUpdate),
@@ -179,10 +191,10 @@ abstract class ScheduleBase extends Action
Timer::tick(
static::ENQUEUE_TIMER * 1000,
fn () => $this->enqueueResources($pools, $dbForConsole, $getProjectDB)
fn () => $this->enqueueResources($pools, $dbForPlatform, $getProjectDB)
);
$this->enqueueResources($pools, $dbForConsole, $getProjectDB);
$this->enqueueResources($pools, $dbForPlatform, $getProjectDB);
});
}
}
@@ -27,7 +27,7 @@ class ScheduleExecutions extends ScheduleBase
return 'executions';
}
protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void
protected function enqueueResources(Group $pools, Database $dbForPlatform, callable $getProjectDB): void
{
$queue = $pools->get('queue')->pop();
$connection = $queue->getResource();
@@ -36,7 +36,7 @@ class ScheduleExecutions extends ScheduleBase
foreach ($this->schedules as $schedule) {
if (!$schedule['active']) {
$dbForConsole->deleteDocument(
$dbForPlatform->deleteDocument(
'schedules',
$schedule['$id'],
);
@@ -50,13 +50,15 @@ class ScheduleExecutions extends ScheduleBase
continue;
}
$data = $dbForConsole->getDocument(
$data = $dbForPlatform->getDocument(
'schedules',
$schedule['$id'],
)->getAttribute('data', []);
$delay = $scheduledAt->getTimestamp() - (new \DateTime())->getTimestamp();
$this->updateProjectAccess($schedule['project'], $dbForPlatform);
\go(function () use ($queueForFunctions, $schedule, $delay, $data) {
Co::sleep($delay);
@@ -74,7 +76,7 @@ class ScheduleExecutions extends ScheduleBase
->trigger();
});
$dbForConsole->deleteDocument(
$dbForPlatform->deleteDocument(
'schedules',
$schedule['$id'],
);
@@ -31,7 +31,7 @@ class ScheduleFunctions extends ScheduleBase
return 'functions';
}
protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void
protected function enqueueResources(Group $pools, Database $dbForPlatform, callable $getProjectDB): void
{
$timerStart = \microtime(true);
$time = DateTime::now();
@@ -70,7 +70,7 @@ class ScheduleFunctions extends ScheduleBase
}
foreach ($delayedExecutions as $delay => $scheduleKeys) {
\go(function () use ($delay, $scheduleKeys, $pools) {
\go(function () use ($delay, $scheduleKeys, $pools, $dbForPlatform) {
\sleep($delay); // in seconds
$queue = $pools->get('queue')->pop();
@@ -84,6 +84,8 @@ class ScheduleFunctions extends ScheduleBase
$schedule = $this->schedules[$scheduleKey];
$this->updateProjectAccess($schedule['project'], $dbForPlatform);
$queueForFunctions = new Func($connection);
$queueForFunctions
@@ -26,7 +26,7 @@ class ScheduleMessages extends ScheduleBase
return 'messages';
}
protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void
protected function enqueueResources(Group $pools, Database $dbForPlatform, callable $getProjectDB): void
{
foreach ($this->schedules as $schedule) {
if (!$schedule['active']) {
@@ -40,18 +40,20 @@ class ScheduleMessages extends ScheduleBase
continue;
}
\go(function () use ($schedule, $pools, $dbForConsole) {
\go(function () use ($schedule, $pools, $dbForPlatform) {
$queue = $pools->get('queue')->pop();
$connection = $queue->getResource();
$queueForMessaging = new Messaging($connection);
$this->updateProjectAccess($schedule['project'], $dbForPlatform);
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($schedule['resourceId'])
->setProject($schedule['project'])
->trigger();
$dbForConsole->deleteDocument(
$dbForPlatform->deleteDocument(
'schedules',
$schedule['$id'],
);
+1 -1
View File
@@ -61,7 +61,7 @@ class Specs extends Action
// Mock dependencies
App::setResource('request', fn () => $this->getRequest());
App::setResource('response', fn () => $response);
App::setResource('dbForConsole', fn () => new Database(new MySQL(''), new Cache(new None())));
App::setResource('dbForPlatform', fn () => new Database(new MySQL(''), new Cache(new None())));
App::setResource('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None())));
$platforms = [
+23 -20
View File
@@ -46,7 +46,7 @@ class Builds extends Action
$this
->desc('Builds worker')
->inject('message')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForEvents')
->inject('queueForFunctions')
->inject('queueForUsage')
@@ -54,12 +54,12 @@ class Builds extends Action
->inject('dbForProject')
->inject('deviceForFunctions')
->inject('log')
->callback(fn ($message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log) => $this->action($message, $dbForConsole, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $log));
->callback(fn ($message, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log) => $this->action($message, $dbForPlatform, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $log));
}
/**
* @param Message $message
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Event $queueForEvents
* @param Func $queueForFunctions
* @param Usage $queueForUsage
@@ -70,7 +70,7 @@ class Builds extends Action
* @return void
* @throws \Utopia\Database\Exception
*/
public function action(Message $message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log): void
public function action(Message $message, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log): void
{
$payload = $message->getPayload() ?? [];
@@ -92,7 +92,7 @@ class Builds extends Action
case BUILD_TYPE_RETRY:
Console::info('Creating build for deployment: ' . $deployment->getId());
$github = new GitHub($cache);
$this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForConsole, $dbForProject, $github, $project, $resource, $deployment, $template, $log);
$this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForPlatform, $dbForProject, $github, $project, $resource, $deployment, $template, $log);
break;
default:
@@ -105,7 +105,7 @@ class Builds extends Action
* @param Func $queueForFunctions
* @param Event $queueForEvents
* @param Usage $queueForUsage
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Database $dbForProject
* @param GitHub $github
* @param Document $project
@@ -117,7 +117,7 @@ class Builds extends Action
* @throws \Utopia\Database\Exception
* @throws Exception
*/
protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForConsole, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log): void
protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForPlatform, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log): void
{
$executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
@@ -199,7 +199,7 @@ class Builds extends Action
$repositoryName = '';
if ($isVcsEnabled) {
$installation = $dbForConsole->getDocument('installations', $installationId);
$installation = $dbForPlatform->getDocument('installations', $installationId);
$providerInstallationId = $installation->getAttribute('providerInstallationId');
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
@@ -209,8 +209,7 @@ class Builds extends Action
try {
if ($isNewBuild && !$isVcsEnabled) {
// Non-vcs+Template
// Non-VCS + Template
$templateRepositoryName = $template->getAttribute('repositoryName', '');
$templateOwnerName = $template->getAttribute('ownerName', '');
$templateVersion = $template->getAttribute('version', '');
@@ -233,6 +232,8 @@ class Builds extends Action
throw new \Exception('Unable to clone code repository: ' . $stderr);
}
Console::execute('find ' . \escapeshellarg($tmpTemplateDirectory) . ' -type d -name ".git" -exec rm -rf {} +', '', $stdout, $stderr);
// Ensure directories
Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $stdout, $stderr);
@@ -398,6 +399,8 @@ class Builds extends Action
throw new \Exception('Repository directory size should be less than ' . number_format($functionsSizeLimit / 1048576, 2) . ' MBs.');
}
Console::execute('find ' . \escapeshellarg($tmpDirectory) . ' -type d -name ".git" -exec rm -rf {} +', '', $stdout, $stderr);
$tarParamDirectory = '/tmp/builds/' . $buildId . '/code' . (empty($rootDirectory) ? '' : '/' . $rootDirectory);
Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $stdout, $stderr); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax
@@ -415,7 +418,7 @@ class Builds extends Action
$directorySize = $deviceForFunctions->getFileSize($source);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttribute('path', $source)->setAttribute('size', $directorySize));
$this->runGitAction('processing', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
$this->runGitAction('processing', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForPlatform);
}
/** Request the executor to build the code... */
@@ -423,7 +426,7 @@ class Builds extends Action
$build = $dbForProject->updateDocument('builds', $buildId, $build);
if ($isVcsEnabled) {
$this->runGitAction('building', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
$this->runGitAction('building', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForPlatform);
}
/** Trigger Webhook */
@@ -637,7 +640,7 @@ class Builds extends Action
$build = $dbForProject->updateDocument('builds', $buildId, $build);
if ($isVcsEnabled) {
$this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
$this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForPlatform);
}
Console::success("Build id: $buildId created");
@@ -658,12 +661,12 @@ class Builds extends Action
/** Update function schedule */
// Inform scheduler if function is still active
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
} catch (\Throwable $th) {
if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
@@ -680,7 +683,7 @@ class Builds extends Action
$build = $dbForProject->updateDocument('builds', $buildId, $build);
if ($isVcsEnabled) {
$this->runGitAction('failed', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
$this->runGitAction('failed', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForPlatform);
}
} finally {
/**
@@ -739,7 +742,7 @@ class Builds extends Action
* @param Document $function
* @param string $deploymentId
* @param Database $dbForProject
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @return void
* @throws Structure
* @throws \Utopia\Database\Exception
@@ -747,7 +750,7 @@ class Builds extends Action
* @throws Conflict
* @throws Restricted
*/
protected function runGitAction(string $status, GitHub $github, string $providerCommitHash, string $owner, string $repositoryName, Document $project, Document $function, string $deploymentId, Database $dbForProject, Database $dbForConsole): void
protected function runGitAction(string $status, GitHub $github, string $providerCommitHash, string $owner, string $repositoryName, Document $project, Document $function, string $deploymentId, Database $dbForProject, Database $dbForPlatform): void
{
if ($function->getAttribute('providerSilentMode', false) === true) {
return;
@@ -792,7 +795,7 @@ class Builds extends Action
$retries++;
try {
$dbForConsole->createDocument('vcsCommentLocks', new Document([
$dbForPlatform->createDocument('vcsCommentLocks', new Document([
'$id' => $commentId
]));
break;
@@ -812,7 +815,7 @@ class Builds extends Action
$comment->addBuild($project, $function, $status, $deployment->getId(), ['type' => 'logs']);
$github->updateComment($owner, $repositoryName, $commentId, $comment->generateComment());
} finally {
$dbForConsole->deleteDocument('vcsCommentLocks', $commentId);
$dbForPlatform->deleteDocument('vcsCommentLocks', $commentId);
}
}
}
+57 -185
View File
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Certificates\Adapter as CertificatesAdapter;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
@@ -11,7 +12,6 @@ use Appwrite\Template\Template;
use Appwrite\Utopia\Response\Model\Rule;
use Exception;
use Throwable;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@@ -43,26 +43,31 @@ class Certificates extends Action
$this
->desc('Certificates worker')
->inject('message')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForMails')
->inject('queueForEvents')
->inject('queueForFunctions')
->inject('log')
->callback(fn (Message $message, Database $dbForConsole, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, Log $log) => $this->action($message, $dbForConsole, $queueForMails, $queueForEvents, $queueForFunctions, $log));
->inject('certificates')
->callback(
fn (Message $message, Database $dbForPlatform, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, Log $log, CertificatesAdapter $certificates) =>
$this->action($message, $dbForPlatform, $queueForMails, $queueForEvents, $queueForFunctions, $log, $certificates)
);
}
/**
* @param Message $message
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Mail $queueForMails
* @param Event $queueForEvents
* @param Func $queueForFunctions
* @param Log $log
* @param CertificatesAdapter $certificates
* @return void
* @throws Throwable
* @throws \Utopia\Database\Exception
*/
public function action(Message $message, Database $dbForConsole, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, Log $log): void
public function action(Message $message, Database $dbForPlatform, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, Log $log, CertificatesAdapter $certificates): void
{
$payload = $message->getPayload() ?? [];
@@ -76,33 +81,33 @@ class Certificates extends Action
$log->addTag('domain', $domain->get());
$this->execute($domain, $dbForConsole, $queueForMails, $queueForEvents, $queueForFunctions, $log, $skipRenewCheck);
$this->execute($domain, $dbForPlatform, $queueForMails, $queueForEvents, $queueForFunctions, $log, $certificates, $skipRenewCheck);
}
/**
* @param Domain $domain
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Mail $queueForMails
* @param Event $queueForEvents
* @param Func $queueForFunctions
* @param CertificatesAdapter $certificates
* @param bool $skipRenewCheck
* @return void
* @throws Throwable
* @throws \Utopia\Database\Exception
*/
private function execute(Domain $domain, Database $dbForConsole, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, Log $log, bool $skipRenewCheck = false): void
private function execute(Domain $domain, Database $dbForPlatform, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, Log $log, CertificatesAdapter $certificates, bool $skipRenewCheck = false): void
{
/**
* 1. Read arguments and validate domain
* 2. Get main domain
* 3. Validate CNAME DNS if parameter is not main domain (meaning it's custom domain)
* 4. Validate security email. Cannot be empty, required by LetsEncrypt
* 5. Validate renew date with certificate file, unless requested to skip by parameter
* 6. Issue a certificate using certbot CLI
* 7. Update 'log' attribute on certificate document with Certbot message
* 8. Create storage folder for certificate, if not ready already
* 9. Move certificates from Certbot location to our Storage
* 10. Create/Update our Storage with new Traefik config with new certificate paths
* 4. Validate renew date with certificate file, unless requested to skip by parameter
* 5. Issue a certificate using certbot CLI
* 6. Update 'log' attribute on certificate document with Certbot message
* 7. Create storage folder for certificate, if not ready already
* 8. Move certificates from Certbot location to our Storage
* 9. Create/Update our Storage with new Traefik config with new certificate paths
* 11. Read certificate file and update 'renewDate' on certificate document
* 12. Update 'issueDate' and 'attempts' on certificate
*
@@ -119,11 +124,11 @@ class Certificates extends Action
* 2. Save document to database
* 3. Update all domains documents with current certificate ID
*
* Note: Renewals are checked and scheduled from maintenence worker
* Note: Renewals are checked and scheduled from maintenance worker
*/
// Get current certificate
$certificate = $dbForConsole->findOne('certificates', [Query::equal('domain', [$domain->get()])]);
$certificate = $dbForPlatform->findOne('certificates', [Query::equal('domain', [$domain->get()])]);
// If we don't have certificate for domain yet, let's create new document. At the end we save it
if ($certificate->isEmpty()) {
@@ -134,40 +139,27 @@ class Certificates extends Action
$success = false;
try {
// Email for alerts is required by LetsEncrypt
$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 an SSL certificate.');
}
// Validate domain and DNS records. Skip if job is forced
if (!$skipRenewCheck) {
$mainDomain = $this->getMainDomain();
$isMainDomain = !isset($mainDomain) || $domain->get() === $mainDomain;
$this->validateDomain($domain, $isMainDomain, $log);
// If certificate exists already, double-check expiry date. Skip if job is forced
if (!$certificates->isRenewRequired($domain->get(), $log)) {
throw new Exception('Renew isn\'t required.');
}
}
// If certificate exists already, double-check expiry date. Skip if job is forced
if (!$skipRenewCheck && !$this->isRenewRequired($domain->get(), $log)) {
throw new Exception('Renew isn\'t required.');
}
// Prepare folder name for certbot. Using this helps prevent miss-match in LetsEncrypt configuration when renewing certificate
$folder = ID::unique();
// Generate certificate files using Let's Encrypt
$letsEncryptData = $this->issueCertificate($folder, $domain->get(), $email);
// Prepare unique cert name. Using this helps prevent miss-match in configuration when renewing certificates.
$certName = ID::unique();
$renewDate = $certificates->issueCertificate($certName, $domain->get());
// Command succeeded, store all data into document
$logs = 'Certificate successfully generated.';
$certificate->setAttribute('logs', \mb_strcut($logs, 0, 1000000));// Limit to 1MB
// Give certificates to Traefik
$this->applyCertificateFiles($folder, $domain->get(), $letsEncryptData);
$certificate->setAttribute('logs', 'Certificate successfully generated.');
// Update certificate info stored in database
$certificate->setAttribute('renewDate', $this->getRenewDate($domain->get()));
$certificate->setAttribute('renewDate', $renewDate);
$certificate->setAttribute('attempts', 0);
$certificate->setAttribute('issueDate', DateTime::now());
$success = true;
@@ -181,7 +173,7 @@ class Certificates extends Action
$attempts = $certificate->getAttribute('attempts', 0) + 1;
$certificate->setAttribute('attempts', $attempts);
// Store cuttent time as renew date to ensure another attempt in next maintenance cycle
// Store current time as renew date to ensure another attempt in next maintenance cycle.
$certificate->setAttribute('renewDate', DateTime::now());
// Send email to security email
@@ -193,7 +185,7 @@ class Certificates extends Action
$certificate->setAttribute('updated', DateTime::now());
// Save all changes we made to certificate document into database
$this->saveCertificateDocument($domain->get(), $certificate, $success, $dbForConsole, $queueForEvents, $queueForFunctions);
$this->saveCertificateDocument($domain->get(), $certificate, $success, $dbForPlatform, $queueForEvents, $queueForFunctions);
}
}
@@ -203,7 +195,7 @@ class Certificates extends Action
* @param string $domain Domain name that certificate is for
* @param Document $certificate Certificate document that we need to save
* @param bool $success
* @param Database $dbForConsole Database connection for console
* @param Database $dbForPlatform Database connection for console
* @param Event $queueForEvents
* @param Func $queueForFunctions
* @return void
@@ -212,21 +204,21 @@ class Certificates extends Action
* @throws Conflict
* @throws Structure
*/
private function saveCertificateDocument(string $domain, Document $certificate, bool $success, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions): void
private function saveCertificateDocument(string $domain, Document $certificate, bool $success, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions): void
{
// Check if update or insert required
$certificateDocument = $dbForConsole->findOne('certificates', [Query::equal('domain', [$domain])]);
$certificateDocument = $dbForPlatform->findOne('certificates', [Query::equal('domain', [$domain])]);
if (!$certificateDocument->isEmpty()) {
// Merge new data with current data
$certificate = new Document(\array_merge($certificateDocument->getArrayCopy(), $certificate->getArrayCopy()));
$certificate = $dbForConsole->updateDocument('certificates', $certificate->getId(), $certificate);
$certificate = $dbForPlatform->updateDocument('certificates', $certificate->getId(), $certificate);
} else {
$certificate->removeAttribute('$internalId');
$certificate = $dbForConsole->createDocument('certificates', $certificate);
$certificate = $dbForPlatform->createDocument('certificates', $certificate);
}
$certificateId = $certificate->getId();
$this->updateDomainDocuments($certificateId, $domain, $success, $dbForConsole, $queueForEvents, $queueForFunctions);
$this->updateDomainDocuments($certificateId, $domain, $success, $dbForPlatform, $queueForEvents, $queueForFunctions);
}
/**
@@ -245,8 +237,8 @@ class Certificates extends Action
}
/**
* Internal domain validation functionality to prevent unnecessary attempts failed from Let's Encrypt side. We check:
* - Domain needs to be public and valid (prevents NFT domains that are not supported by Let's Encrypt)
* Internal domain validation functionality to prevent unnecessary attempts. We check:
* - Domain needs to be public and valid (prevents NFT domains that are not supported)
* - Domain must have proper DNS record
*
* @param Domain $domain Domain which we validate
@@ -292,136 +284,6 @@ class Certificates extends Action
}
}
/**
* Reads expiry date of certificate from file and decides if renewal is required or not.
*
* @param string $domain Domain for which we check certificate file
* @return bool True, if certificate needs to be renewed
* @throws Exception
*/
private function isRenewRequired(string $domain, Log $log): bool
{
$certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem';
if (\file_exists($certPath)) {
$validTo = null;
$certData = openssl_x509_parse(file_get_contents($certPath));
$validTo = $certData['validTo_time_t'] ?? 0;
if (empty($validTo)) {
$log->addTag('certificateDomain', $domain);
throw new Exception('Unable to read certificate file (cert.pem).');
}
// LetsEncrypt allows renewal 30 days before expiry
$expiryInAdvance = (60 * 60 * 24 * 30);
if ($validTo - $expiryInAdvance > \time()) {
$log->addTag('certificateDomain', $domain);
$log->addExtra('certificateData', \is_array($certData) ? \json_encode($certData) : \strval($certData));
return false;
}
}
return true;
}
/**
* LetsEncrypt communication to issue certificate (using certbot CLI)
*
* @param string $folder Folder into which certificates should be generated
* @param string $domain Domain to generate certificate for
* @return array Named array with keys 'stdout' and 'stderr', both string
* @throws Exception
*/
private function issueCertificate(string $folder, string $domain, string $email): array
{
$stdout = '';
$stderr = '';
$staging = (App::isProduction()) ? '' : ' --dry-run';
$exit = Console::execute("certbot certonly -v --webroot --noninteractive --agree-tos{$staging}"
. " --email " . $email
. " --cert-name " . $folder
. " -w " . APP_STORAGE_CERTIFICATES
. " -d {$domain}", '', $stdout, $stderr);
// Unexpected error, usually 5XX, API limits, ...
if ($exit !== 0) {
throw new Exception('Failed to issue a certificate with message: ' . $stderr);
}
return [
'stdout' => $stdout,
'stderr' => $stderr
];
}
/**
* Read new renew date from certificate file generated by Let's Encrypt
*
* @param string $domain Domain which certificate was generated for
* @return string
* @throws \Utopia\Database\Exception
*/
private function getRenewDate(string $domain): string
{
$certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem';
$certData = openssl_x509_parse(file_get_contents($certPath));
$validTo = $certData['validTo_time_t'] ?? null;
$dt = (new \DateTime())->setTimestamp($validTo);
return DateTime::addSeconds($dt, -60 * 60 * 24 * 30); // -30 days
}
/**
* Method to take files from Let's Encrypt, and put it into Traefik.
*
* @param string $domain Domain which certificate was generated for
* @param string $folder Folder in which certificates were generated
* @param array $letsEncryptData Let's Encrypt logs to use for additional info when throwing error
* @return void
* @throws Exception
*/
private function applyCertificateFiles(string $folder, string $domain, array $letsEncryptData): void
{
// Prepare folder in storage for domain
$path = APP_STORAGE_CERTIFICATES . '/' . $domain;
if (!\is_readable($path)) {
if (!\mkdir($path, 0755, true)) {
throw new Exception('Failed to create path for certificate.');
}
}
// Move generated files
if (!@\rename('/etc/letsencrypt/live/' . $folder . '/cert.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem')) {
throw new Exception('Failed to rename certificate cert.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']);
}
if (!@\rename('/etc/letsencrypt/live/' . $folder . '/chain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/chain.pem')) {
throw new Exception('Failed to rename certificate chain.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']);
}
if (!@\rename('/etc/letsencrypt/live/' . $folder . '/fullchain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/fullchain.pem')) {
throw new Exception('Failed to rename certificate fullchain.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']);
}
if (!@\rename('/etc/letsencrypt/live/' . $folder . '/privkey.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/privkey.pem')) {
throw new Exception('Failed to rename certificate privkey.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']);
}
$config = \implode(PHP_EOL, [
"tls:",
" certificates:",
" - certFile: /storage/certificates/{$domain}/fullchain.pem",
" keyFile: /storage/certificates/{$domain}/privkey.pem"
]);
// Save configuration into Traefik using our new cert files
if (!\file_put_contents(APP_STORAGE_CONFIG . '/' . $domain . '.yml', $config)) {
throw new Exception('Failed to save Traefik configuration.');
}
}
/**
* Method to make sure information about error is delivered to admnistrator.
*
@@ -475,15 +337,21 @@ class Certificates extends Action
*
* @return void
*/
private function updateDomainDocuments(string $certificateId, string $domain, bool $success, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions): void
private function updateDomainDocuments(string $certificateId, string $domain, bool $success, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions): void
{
$rule = $dbForConsole->getDocument('rules', md5($domain));
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$rule = $dbForPlatform->getDocument('rules', md5($domain));
} else {
$rule = $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain]),
]);
}
if (!$rule->isEmpty()) {
$rule->setAttribute('certificateId', $certificateId);
$rule->setAttribute('status', $success ? 'verified' : 'unverified');
$dbForConsole->updateDocument('rules', $rule->getId(), $rule);
$dbForPlatform->updateDocument('rules', $rule->getId(), $rule);
$projectId = $rule->getAttribute('projectId');
@@ -492,7 +360,11 @@ class Certificates extends Action
return;
}
$project = $dbForConsole->getDocument('projects', $projectId);
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
return;
}
/** Trigger Webhook */
$ruleModel = new Rule();
+45 -38
View File
@@ -11,6 +11,7 @@ use Utopia\Database\Document;
use Utopia\Database\Exception as DatabaseException;
use Utopia\Database\Exception\Authorization;
use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\NotFound;
use Utopia\Database\Exception\Restricted;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Query;
@@ -33,21 +34,21 @@ class Databases extends Action
$this
->desc('Databases worker')
->inject('message')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('dbForProject')
->inject('log')
->callback(fn (Message $message, Database $dbForConsole, Database $dbForProject, Log $log) => $this->action($message, $dbForConsole, $dbForProject, $log));
->callback(fn (Message $message, Database $dbForPlatform, Database $dbForProject, Log $log) => $this->action($message, $dbForPlatform, $dbForProject, $log));
}
/**
* @param Message $message
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Database $dbForProject
* @param Log $log
* @return void
* @throws \Exception
*/
public function action(Message $message, Database $dbForConsole, Database $dbForProject, Log $log): void
public function action(Message $message, Database $dbForPlatform, Database $dbForProject, Log $log): void
{
$payload = $message->getPayload() ?? [];
@@ -73,10 +74,10 @@ class Databases extends Action
match (\strval($type)) {
DATABASE_TYPE_DELETE_DATABASE => $this->deleteDatabase($database, $project, $dbForProject),
DATABASE_TYPE_DELETE_COLLECTION => $this->deleteCollection($database, $collection, $project, $dbForProject),
DATABASE_TYPE_CREATE_ATTRIBUTE => $this->createAttribute($database, $collection, $document, $project, $dbForConsole, $dbForProject),
DATABASE_TYPE_DELETE_ATTRIBUTE => $this->deleteAttribute($database, $collection, $document, $project, $dbForConsole, $dbForProject),
DATABASE_TYPE_CREATE_INDEX => $this->createIndex($database, $collection, $document, $project, $dbForConsole, $dbForProject),
DATABASE_TYPE_DELETE_INDEX => $this->deleteIndex($database, $collection, $document, $project, $dbForConsole, $dbForProject),
DATABASE_TYPE_CREATE_ATTRIBUTE => $this->createAttribute($database, $collection, $document, $project, $dbForPlatform, $dbForProject),
DATABASE_TYPE_DELETE_ATTRIBUTE => $this->deleteAttribute($database, $collection, $document, $project, $dbForPlatform, $dbForProject),
DATABASE_TYPE_CREATE_INDEX => $this->createIndex($database, $collection, $document, $project, $dbForPlatform, $dbForProject),
DATABASE_TYPE_DELETE_INDEX => $this->deleteIndex($database, $collection, $document, $project, $dbForPlatform, $dbForProject),
default => throw new \Exception('No database operation for type: ' . \strval($type)),
};
}
@@ -86,7 +87,7 @@ class Databases extends Action
* @param Document $collection
* @param Document $attribute
* @param Document $project
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Database $dbForProject
* @return void
* @throws Authorization
@@ -94,7 +95,7 @@ class Databases extends Action
* @throws \Exception
* @throws \Throwable
*/
private function createAttribute(Document $database, Document $collection, Document $attribute, Document $project, Database $dbForConsole, Database $dbForProject): void
private function createAttribute(Document $database, Document $collection, Document $attribute, Document $project, Database $dbForPlatform, Database $dbForProject): void
{
if ($collection->isEmpty()) {
throw new Exception('Missing collection');
@@ -133,7 +134,7 @@ class Databases extends Action
$formatOptions = $attribute->getAttribute('formatOptions', []);
$filters = $attribute->getAttribute('filters', []);
$options = $attribute->getAttribute('options', []);
$project = $dbForConsole->getDocument('projects', $projectId);
$project = $dbForPlatform->getDocument('projects', $projectId);
try {
switch ($type) {
@@ -210,7 +211,7 @@ class Databases extends Action
* @param Document $collection
* @param Document $attribute
* @param Document $project
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Database $dbForProject
* @return void
* @throws Authorization
@@ -218,7 +219,7 @@ class Databases extends Action
* @throws \Exception
* @throws \Throwable
**/
private function deleteAttribute(Document $database, Document $collection, Document $attribute, Document $project, Database $dbForConsole, Database $dbForProject): void
private function deleteAttribute(Document $database, Document $collection, Document $attribute, Document $project, Database $dbForPlatform, Database $dbForProject): void
{
if ($collection->isEmpty()) {
throw new Exception('Missing collection');
@@ -238,7 +239,7 @@ class Databases extends Action
$key = $attribute->getAttribute('key', '');
$status = $attribute->getAttribute('status', '');
$type = $attribute->getAttribute('type', '');
$project = $dbForConsole->getDocument('projects', $projectId);
$project = $dbForPlatform->getDocument('projects', $projectId);
$options = $attribute->getAttribute('options', []);
$relatedAttribute = new Document();
$relatedCollection = new Document();
@@ -251,23 +252,21 @@ class Databases extends Action
try {
try {
if ($status !== 'failed') {
if ($type === Database::VAR_RELATIONSHIP) {
if ($options['twoWay']) {
$relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $options['relatedCollection']);
if ($relatedCollection->isEmpty()) {
throw new DatabaseException('Collection not found');
}
$relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']);
if ($type === Database::VAR_RELATIONSHIP) {
if ($options['twoWay']) {
$relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $options['relatedCollection']);
if ($relatedCollection->isEmpty()) {
throw new DatabaseException('Collection not found');
}
if (!$dbForProject->deleteRelationship('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'stuck'));
throw new DatabaseException('Failed to delete Relationship');
}
} elseif (!$dbForProject->deleteAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
throw new DatabaseException('Failed to delete Attribute');
$relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']);
}
if (!$dbForProject->deleteRelationship('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'stuck'));
throw new DatabaseException('Failed to delete Relationship');
}
} elseif (!$dbForProject->deleteAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
throw new DatabaseException('Failed to delete Attribute');
}
$dbForProject->deleteDocument('attributes', $attribute->getId());
@@ -275,6 +274,16 @@ class Databases extends Action
if (!$relatedAttribute->isEmpty()) {
$dbForProject->deleteDocument('attributes', $relatedAttribute->getId());
}
} catch (NotFound $e) {
Console::error($e->getMessage());
$dbForProject->deleteDocument('attributes', $attribute->getId());
if (!$relatedAttribute->isEmpty()) {
$dbForProject->deleteDocument('attributes', $relatedAttribute->getId());
}
} catch (\Throwable $e) {
Console::error($e->getMessage());
@@ -345,7 +354,7 @@ class Databases extends Action
}
if ($exists) { // Delete the duplicate if created, else update in db
$this->deleteIndex($database, $collection, $index, $project, $dbForConsole, $dbForProject);
$this->deleteIndex($database, $collection, $index, $project, $dbForPlatform, $dbForProject);
} else {
$dbForProject->updateDocument('indexes', $index->getId(), $index);
}
@@ -354,11 +363,9 @@ class Databases extends Action
}
} finally {
$dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId);
$dbForProject->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId());
if (!$relatedCollection->isEmpty() && !$relatedAttribute->isEmpty()) {
$dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $relatedCollection->getId());
$dbForProject->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId());
}
}
}
@@ -368,7 +375,7 @@ class Databases extends Action
* @param Document $collection
* @param Document $index
* @param Document $project
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Database $dbForProject
* @return void
* @throws Authorization
@@ -377,7 +384,7 @@ class Databases extends Action
* @throws DatabaseException
* @throws \Throwable
*/
private function createIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForConsole, Database $dbForProject): void
private function createIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForPlatform, Database $dbForProject): void
{
if ($collection->isEmpty()) {
throw new Exception('Missing collection');
@@ -399,7 +406,7 @@ class Databases extends Action
$attributes = $index->getAttribute('attributes', []);
$lengths = $index->getAttribute('lengths', []);
$orders = $index->getAttribute('orders', []);
$project = $dbForConsole->getDocument('projects', $projectId);
$project = $dbForPlatform->getDocument('projects', $projectId);
try {
if (!$dbForProject->createIndex('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key, $type, $attributes, $lengths, $orders)) {
@@ -429,7 +436,7 @@ class Databases extends Action
* @param Document $collection
* @param Document $index
* @param Document $project
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Database $dbForProject
* @return void
* @throws Authorization
@@ -438,7 +445,7 @@ class Databases extends Action
* @throws DatabaseException
* @throws \Throwable
*/
private function deleteIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForConsole, Database $dbForProject): void
private function deleteIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForPlatform, Database $dbForProject): void
{
if ($collection->isEmpty()) {
throw new Exception('Missing collection');
@@ -456,7 +463,7 @@ class Databases extends Action
]);
$key = $index->getAttribute('key');
$status = $index->getAttribute('status', '');
$project = $dbForConsole->getDocument('projects', $projectId);
$project = $dbForPlatform->getDocument('projects', $projectId);
try {
if ($status !== 'failed' && !$dbForProject->deleteIndex('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
+87 -112
View File
@@ -3,11 +3,10 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Auth\Auth;
use Appwrite\Certificates\Adapter as CertificatesAdapter;
use Appwrite\Extend\Exception;
use Executor\Executor;
use Throwable;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\Database\TimeLimit;
use Utopia\Audit\Audit;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
@@ -44,24 +43,28 @@ class Deletes extends Action
$this
->desc('Deletes worker')
->inject('message')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('timelimit')
->inject('deviceForFiles')
->inject('deviceForFunctions')
->inject('deviceForBuilds')
->inject('deviceForCache')
->inject('abuseRetention')
->inject('certificates')
->inject('executionRetention')
->inject('auditRetention')
->inject('log')
->callback(fn ($message, $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log) => $this->action($message, $dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $abuseRetention, $executionRetention, $auditRetention, $log));
->callback(
fn ($message, $dbForPlatform, callable $getProjectDB, callable $timelimit, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, CertificatesAdapter $certificates, string $executionRetention, string $auditRetention, Log $log) =>
$this->action($message, $dbForPlatform, $getProjectDB, $timelimit, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $certificates, $executionRetention, $auditRetention, $log)
);
}
/**
* @throws Exception
* @throws Throwable
*/
public function action(Message $message, Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log): void
public function action(Message $message, Database $dbForPlatform, callable $getProjectDB, callable $timelimit, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, CertificatesAdapter $certificates, string $executionRetention, string $auditRetention, Log $log): void
{
$payload = $message->getPayload() ?? [];
@@ -84,10 +87,10 @@ class Deletes extends Action
case DELETE_TYPE_DOCUMENT:
switch ($document->getCollection()) {
case DELETE_TYPE_PROJECTS:
$this->deleteProject($dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $document);
$this->deleteProject($dbForPlatform, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $certificates, $document);
break;
case DELETE_TYPE_FUNCTIONS:
$this->deleteFunction($dbForConsole, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $document, $project);
$this->deleteFunction($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $certificates, $document, $project);
break;
case DELETE_TYPE_DEPLOYMENTS:
$this->deleteDeployment($getProjectDB, $deviceForFunctions, $deviceForBuilds, $document, $project);
@@ -99,10 +102,10 @@ class Deletes extends Action
$this->deleteBucket($getProjectDB, $deviceForFiles, $document, $project);
break;
case DELETE_TYPE_INSTALLATIONS:
$this->deleteInstallation($dbForConsole, $getProjectDB, $document, $project);
$this->deleteInstallation($dbForPlatform, $getProjectDB, $document, $project);
break;
case DELETE_TYPE_RULES:
$this->deleteRule($dbForConsole, $document);
$this->deleteRule($dbForPlatform, $document, $certificates);
break;
default:
Console::error('No lazy delete operation available for document of type: ' . $document->getCollection());
@@ -110,7 +113,7 @@ class Deletes extends Action
}
break;
case DELETE_TYPE_TEAM_PROJECTS:
$this->deleteProjectsByTeam($dbForConsole, $getProjectDB, $document);
$this->deleteProjectsByTeam($dbForPlatform, $getProjectDB, $certificates, $document);
break;
case DELETE_TYPE_EXECUTIONS:
$this->deleteExecutionLogs($project, $getProjectDB, $executionRetention);
@@ -120,11 +123,8 @@ class Deletes extends Action
$this->deleteAuditLogs($project, $getProjectDB, $auditRetention);
}
break;
case DELETE_TYPE_ABUSE:
$this->deleteAbuseLogs($project, $getProjectDB, $abuseRetention);
break;
case DELETE_TYPE_REALTIME:
$this->deleteRealtimeUsage($dbForConsole, $datetime);
$this->deleteRealtimeUsage($dbForPlatform, $datetime);
break;
case DELETE_TYPE_SESSIONS:
$this->deleteExpiredSessions($project, $getProjectDB);
@@ -139,7 +139,7 @@ class Deletes extends Action
$this->deleteCacheByDate($project, $getProjectDB, $datetime);
break;
case DELETE_TYPE_SCHEDULES:
$this->deleteSchedules($dbForConsole, $getProjectDB, $datetime);
$this->deleteSchedules($dbForPlatform, $getProjectDB, $datetime);
break;
case DELETE_TYPE_TOPIC:
$this->deleteTopic($project, $getProjectDB, $document);
@@ -159,7 +159,7 @@ class Deletes extends Action
}
/**
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param callable $getProjectDB
* @param string $datetime
* @param Document|null $document
@@ -170,7 +170,7 @@ class Deletes extends Action
* @throws Structure
* @throws DatabaseException
*/
private function deleteSchedules(Database $dbForConsole, callable $getProjectDB, string $datetime): void
private function deleteSchedules(Database $dbForPlatform, callable $getProjectDB, string $datetime): void
{
$this->listByGroup(
'schedules',
@@ -179,12 +179,12 @@ class Deletes extends Action
Query::lessThanEqual('resourceUpdatedAt', $datetime),
Query::equal('active', [false]),
],
$dbForConsole,
function (Document $document) use ($dbForConsole, $getProjectDB) {
$project = $dbForConsole->getDocument('projects', $document->getAttribute('projectId'));
$dbForPlatform,
function (Document $document) use ($dbForPlatform, $getProjectDB) {
$project = $dbForPlatform->getDocument('projects', $document->getAttribute('projectId'));
if ($project->isEmpty()) {
$dbForConsole->deleteDocument('schedules', $document->getId());
$dbForPlatform->deleteDocument('schedules', $document->getId());
Console::success('Deleted schedule for deleted project ' . $document->getAttribute('projectId'));
return;
}
@@ -208,7 +208,7 @@ class Deletes extends Action
}
if ($delete) {
$dbForConsole->deleteDocument('schedules', $document->getId());
$dbForPlatform->deleteDocument('schedules', $document->getId());
Console::success('Deleting schedule for ' . $document->getAttribute('resourceType') . ' ' . $document->getAttribute('resourceId'));
}
}
@@ -264,7 +264,7 @@ class Deletes extends Action
MESSAGE_TYPE_EMAIL => 'emailTotal',
MESSAGE_TYPE_SMS => 'smsTotal',
MESSAGE_TYPE_PUSH => 'pushTotal',
default => throw new Exception('Invalid target provider type'),
default => throw new Exception('Invalid target CertificatesAdapter type'),
};
$dbForProject->decreaseDocumentAttribute(
'topics',
@@ -389,7 +389,7 @@ class Deletes extends Action
}
/**
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param callable $getProjectDB
* @param string $hourlyUsageRetentionDatetime
* @return void
@@ -432,7 +432,7 @@ class Deletes extends Action
}
/**
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Document $document
* @return void
* @throws Authorization
@@ -442,10 +442,10 @@ class Deletes extends Action
* @throws Structure
* @throws Exception
*/
private function deleteProjectsByTeam(Database $dbForConsole, callable $getProjectDB, Document $document): void
private function deleteProjectsByTeam(Database $dbForPlatform, callable $getProjectDB, CertificatesAdapter $certificates, Document $document): void
{
$projects = $dbForConsole->find('projects', [
$projects = $dbForPlatform->find('projects', [
Query::equal('teamInternalId', [$document->getInternalId()])
]);
@@ -455,13 +455,13 @@ class Deletes extends Action
$deviceForBuilds = getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId());
$deviceForCache = getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId());
$this->deleteProject($dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $project);
$dbForConsole->deleteDocument('projects', $project->getId());
$this->deleteProject($dbForPlatform, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $certificates, $project);
$dbForPlatform->deleteDocument('projects', $project->getId());
}
}
/**
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param callable $getProjectDB
* @param Device $deviceForFiles
* @param Device $deviceForFunctions
@@ -473,7 +473,7 @@ class Deletes extends Action
* @throws Authorization
* @throws DatabaseException
*/
private function deleteProject(Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, Document $document): void
private function deleteProject(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, CertificatesAdapter $certificates, Document $document): void
{
$projectInternalId = $document->getInternalId();
$projectId = $document->getId();
@@ -489,35 +489,35 @@ class Deletes extends Action
$projectCollectionIds = [
...\array_keys(Config::getParam('collections', [])['projects']),
Audit::COLLECTION,
TimeLimit::COLLECTION,
Audit::COLLECTION
];
$limit = \count($projectCollectionIds) + 25;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
$sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', ''));
$projectTables = !\in_array($dsn->getHost(), $sharedTables);
$sharedTablesV1 = \in_array($dsn->getHost(), $sharedTablesV1);
$sharedTablesV2 = !$projectTables && !$sharedTablesV1;
$sharedTables = $sharedTablesV1 || $sharedTablesV2;
while (true) {
$collections = $dbForProject->listCollections($limit);
foreach ($collections as $collection) {
if ($dsn->getHost() !== System::getEnv('_APP_DATABASE_SHARED_TABLES', '') || !\in_array($collection->getId(), $projectCollectionIds)) {
try {
try {
if ($projectTables || !\in_array($collection->getId(), $projectCollectionIds)) {
$dbForProject->deleteCollection($collection->getId());
} catch (Throwable $e) {
Console::error('Error deleting '.$collection->getId().' '.$e->getMessage());
/**
* Ignore junction tables;
*/
if (!preg_match('/^_\d+_\d+$/', $collection->getId())) {
throw $e;
}
} else {
$this->deleteByGroup($collection->getId(), [], database: $dbForProject);
}
} else {
$this->deleteByGroup($collection->getId(), [], database: $dbForProject);
} catch (Throwable $e) {
Console::error('Error deleting '.$collection->getId().' '.$e->getMessage());
}
}
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
if ($sharedTables) {
$collectionsIds = \array_map(fn ($collection) => $collection->getId(), $collections);
if (empty(\array_diff($collectionsIds, $projectCollectionIds))) {
@@ -531,50 +531,57 @@ class Deletes extends Action
// Delete Platforms
$this->deleteByGroup('platforms', [
Query::equal('projectInternalId', [$projectInternalId])
], $dbForConsole);
], $dbForPlatform);
// Delete project and function rules
$this->deleteByGroup('rules', [
Query::equal('projectInternalId', [$projectInternalId])
], $dbForConsole, function (Document $document) use ($dbForConsole) {
$this->deleteRule($dbForConsole, $document);
], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) {
$this->deleteRule($dbForPlatform, $document, $certificates);
});
// Delete Keys
$this->deleteByGroup('keys', [
Query::equal('projectInternalId', [$projectInternalId])
], $dbForConsole);
], $dbForPlatform);
// Delete Webhooks
$this->deleteByGroup('webhooks', [
Query::equal('projectInternalId', [$projectInternalId])
], $dbForConsole);
], $dbForPlatform);
// Delete VCS Installations
$this->deleteByGroup('installations', [
Query::equal('projectInternalId', [$projectInternalId])
], $dbForConsole);
], $dbForPlatform);
// Delete VCS Repositories
$this->deleteByGroup('repositories', [
Query::equal('projectInternalId', [$projectInternalId]),
], $dbForConsole);
], $dbForPlatform);
// Delete VCS comments
$this->deleteByGroup('vcsComments', [
Query::equal('projectInternalId', [$projectInternalId]),
], $dbForConsole);
], $dbForPlatform);
// Delete Schedules (No projectInternalId in this collection)
$this->deleteByGroup('schedules', [
Query::equal('projectId', [$projectId]),
], $dbForConsole);
], $dbForPlatform);
// Delete metadata table
if ($dsn->getHost() !== System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$dbForProject->deleteCollection('_metadata');
} else {
$this->deleteByGroup('_metadata', [], $dbForProject);
if ($projectTables) {
$dbForProject->deleteCollection(Database::METADATA);
} elseif ($sharedTablesV1) {
$this->deleteByGroup(Database::METADATA, [], $dbForProject);
} elseif ($sharedTablesV2) {
$queries = \array_map(
fn ($id) => Query::notEqual('$id', $id),
$projectCollectionIds
);
$this->deleteByGroup(Database::METADATA, $queries, $dbForProject);
}
// Delete all storage directories
@@ -641,7 +648,7 @@ class Deletes extends Action
}
/**
* @param database $dbForConsole
* @param database $dbForPlatform
* @param callable $getProjectDB
* @param string $datetime
* @return void
@@ -657,7 +664,7 @@ class Deletes extends Action
}
/**
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param callable $getProjectDB
* @return void
* @throws Exception|Throwable
@@ -675,42 +682,21 @@ class Deletes extends Action
}
/**
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param string $datetime
* @return void
* @throws Exception
*/
private function deleteRealtimeUsage(Database $dbForConsole, string $datetime): void
private function deleteRealtimeUsage(Database $dbForPlatform, string $datetime): void
{
// Delete Dead Realtime Logs
$this->deleteByGroup('realtime', [
Query::lessThan('timestamp', $datetime)
], $dbForConsole);
], $dbForPlatform);
}
/**
* @param Database $dbForConsole
* @param callable $getProjectDB
* @param string $datetime
* @return void
* @throws Exception
*/
private function deleteAbuseLogs(Document $project, callable $getProjectDB, string $abuseRetention): void
{
$projectId = $project->getId();
$dbForProject = $getProjectDB($project);
$timeLimit = new TimeLimit("", 0, 1, $dbForProject);
$abuse = new Abuse($timeLimit);
try {
$abuse->cleanup($abuseRetention);
} catch (DatabaseException $e) {
Console::error('Failed to delete abuse logs for project ' . $projectId . ': ' . $e->getMessage());
}
}
/**
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param callable $getProjectDB
* @param string $datetime
* @return void
@@ -738,7 +724,7 @@ class Deletes extends Action
* @return void
* @throws Exception
*/
private function deleteFunction(Database $dbForConsole, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, Document $document, Document $project): void
private function deleteFunction(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, CertificatesAdapter $certificates, Document $document, Document $project): void
{
$projectId = $project->getId();
$dbForProject = $getProjectDB($project);
@@ -753,8 +739,8 @@ class Deletes extends Action
Query::equal('resourceType', ['function']),
Query::equal('resourceInternalId', [$functionInternalId]),
Query::equal('projectInternalId', [$project->getInternalId()])
], $dbForConsole, function (Document $document) use ($project, $dbForConsole) {
$this->deleteRule($dbForConsole, $document);
], $dbForPlatform, function (Document $document) use ($project, $dbForPlatform, $certificates) {
$this->deleteRule($dbForPlatform, $document, $certificates);
});
/**
@@ -808,13 +794,13 @@ class Deletes extends Action
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::equal('resourceInternalId', [$functionInternalId]),
Query::equal('resourceType', ['function']),
], $dbForConsole, function (Document $document) use ($dbForConsole) {
], $dbForPlatform, function (Document $document) use ($dbForPlatform) {
$providerRepositoryId = $document->getAttribute('providerRepositoryId', '');
$projectInternalId = $document->getAttribute('projectInternalId', '');
$this->deleteByGroup('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('projectInternalId', [$projectInternalId]),
], $dbForConsole);
], $dbForPlatform);
});
/**
@@ -1036,29 +1022,18 @@ class Deletes extends Action
}
/**
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Document $document rule document
* @return void
*/
private function deleteRule(Database $dbForConsole, Document $document): void
private function deleteRule(Database $dbForPlatform, Document $document, CertificatesAdapter $certificates): void
{
$domain = $document->getAttribute('domain');
$directory = APP_STORAGE_CERTIFICATES . '/' . $domain;
$checkTraversal = realpath($directory) === $directory;
if ($checkTraversal && is_dir($directory)) {
// Delete files, so Traefik is aware of change
array_map('unlink', glob($directory . '/*.*'));
rmdir($directory);
Console::info("Deleted certificate files for {$domain}");
} else {
Console::info("No certificate files found for {$domain}");
}
$certificates->deleteCertificate($domain);
// Delete certificate document, so Appwrite is aware of change
if (isset($document['certificateId'])) {
$dbForConsole->deleteDocument('certificates', $document['certificateId']);
$dbForPlatform->deleteDocument('certificates', $document['certificateId']);
}
}
@@ -1079,21 +1054,21 @@ class Deletes extends Action
}
/**
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param callable $getProjectDB
* @param Document $document
* @param Document $project
* @return void
* @throws Exception
*/
private function deleteInstallation(Database $dbForConsole, callable $getProjectDB, Document $document, Document $project): void
private function deleteInstallation(Database $dbForPlatform, callable $getProjectDB, Document $document, Document $project): void
{
$dbForProject = $getProjectDB($project);
$this->listByGroup('functions', [
Query::equal('installationInternalId', [$document->getInternalId()])
], $dbForProject, function ($function) use ($dbForProject, $dbForConsole) {
$dbForConsole->deleteDocument('repositories', $function->getAttribute('repositoryId'));
], $dbForProject, function ($function) use ($dbForProject, $dbForPlatform) {
$dbForPlatform->deleteDocument('repositories', $function->getAttribute('repositoryId'));
$function = $function
->setAttribute('installationId', '')
+51 -27
View File
@@ -16,7 +16,6 @@ use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\Restricted;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Helpers\ID;
use Utopia\Logger\Log;
use Utopia\Migration\Destination;
use Utopia\Migration\Destinations\Appwrite as DestinationAppwrite;
use Utopia\Migration\Exception as MigrationException;
@@ -33,10 +32,12 @@ class Migrations extends Action
{
protected Database $dbForProject;
protected Database $dbForConsole;
protected Database $dbForPlatform;
protected Document $project;
protected $logError;
public static function getName(): string
{
return 'migrations';
@@ -51,15 +52,15 @@ class Migrations extends Action
->desc('Migrations worker')
->inject('message')
->inject('dbForProject')
->inject('dbForConsole')
->inject('log')
->callback(fn (Message $message, Database $dbForProject, Database $dbForConsole, Log $log) => $this->action($message, $dbForProject, $dbForConsole, $log));
->inject('dbForPlatform')
->inject('logError')
->callback(fn (Message $message, Database $dbForProject, Database $dbForPlatform, callable $logError) => $this->action($message, $dbForProject, $dbForPlatform, $logError));
}
/**
* @throws Exception
*/
public function action(Message $message, Database $dbForProject, Database $dbForConsole, Log $log): void
public function action(Message $message, Database $dbForProject, Database $dbForPlatform, callable $logError): void
{
$payload = $message->getPayload() ?? [];
@@ -76,8 +77,9 @@ class Migrations extends Action
}
$this->dbForProject = $dbForProject;
$this->dbForConsole = $dbForConsole;
$this->dbForPlatform = $dbForPlatform;
$this->project = $project;
$this->logError = $logError;
/**
* Handle Event execution.
@@ -86,10 +88,7 @@ class Migrations extends Action
return;
}
$log->addTag('migrationId', $migration->getId());
$log->addTag('projectId', $project->getId());
$this->processMigration($migration, $log);
$this->processMigration($migration);
}
/**
@@ -198,7 +197,7 @@ class Migrations extends Action
*/
protected function removeAPIKey(Document $apiKey): void
{
$this->dbForConsole->deleteDocument('keys', $apiKey->getId());
$this->dbForPlatform->deleteDocument('keys', $apiKey->getId());
}
/**
@@ -245,8 +244,8 @@ class Migrations extends Action
'secret' => $generatedSecret,
]);
$this->dbForConsole->createDocument('keys', $key);
$this->dbForConsole->purgeCachedDocument('projects', $project->getId());
$this->dbForPlatform->createDocument('keys', $key);
$this->dbForPlatform->purgeCachedDocument('projects', $project->getId());
return $key;
}
@@ -259,10 +258,10 @@ class Migrations extends Action
* @throws \Utopia\Database\Exception
* @throws Exception
*/
protected function processMigration(Document $migration, Log $log): void
protected function processMigration(Document $migration): void
{
$project = $this->project;
$projectDocument = $this->dbForConsole->getDocument('projects', $project->getId());
$projectDocument = $this->dbForPlatform->getDocument('projects', $project->getId());
$tempAPIKey = $this->generateAPIKey($projectDocument);
$transfer = $source = $destination = null;
@@ -285,8 +284,6 @@ class Migrations extends Action
$migration->setAttribute('status', 'processing');
$this->updateMigrationDocument($migration, $projectDocument);
$log->addTag('type', $migration->getAttribute('source'));
$source = $this->processSource($migration);
$destination = $this->processDestination($migration, $tempAPIKey->getAttribute('secret'));
@@ -324,7 +321,6 @@ class Migrations extends Action
$errorMessages = [];
foreach ($sourceErrors as $error) {
/** @var $sourceErrors $error */
$message = "Error occurred while fetching '{$error->getResourceName()}:{$error->getResourceId()}' from source with message: '{$error->getMessage()}'";
if ($error->getPrevious()) {
$message .= " Message: ".$error->getPrevious()->getMessage() . " File: ".$error->getPrevious()->getFile() . " Line: ".$error->getPrevious()->getLine();
@@ -344,7 +340,6 @@ class Migrations extends Action
}
$migration->setAttribute('errors', $errorMessages);
$log->addExtra('migrationErrors', json_encode($errorMessages));
$this->updateMigrationDocument($migration, $projectDocument);
return;
@@ -359,7 +354,12 @@ class Migrations extends Action
if (! $migration->isEmpty()) {
$migration->setAttribute('status', 'failed');
$migration->setAttribute('stage', 'finished');
$migration->setAttribute('errors', [$th->getMessage()]);
call_user_func($this->logError, $th, 'appwrite-worker', 'appwrite-queue-'.self::getName(), [
'migrationId' => $migration->getId(),
'source' => $migration->getAttribute('source') ?? '',
'destination' => $migration->getAttribute('destination') ?? '',
]);
return;
}
@@ -379,7 +379,6 @@ class Migrations extends Action
}
$migration->setAttribute('errors', $errorMessages);
$log->addTag('migrationErrors', json_encode($errorMessages));
}
} finally {
if (! $tempAPIKey->isEmpty()) {
@@ -391,15 +390,40 @@ class Migrations extends Action
if ($migration->getAttribute('status', '') === 'failed') {
Console::error('Migration('.$migration->getInternalId().':'.$migration->getId().') failed, Project('.$this->project->getInternalId().':'.$this->project->getId().')');
$destination->error();
$source->error();
if ($destination) {
$destination->error();
throw new Exception('Migration failed');
foreach ($destination->getErrors() as $error) {
/** @var MigrationException $error */
call_user_func($this->logError, $error, 'appwrite-worker', 'appwrite-queue-' . self::getName(), [
'migrationId' => $migration->getId(),
'source' => $migration->getAttribute('source') ?? '',
'destination' => $migration->getAttribute('destination') ?? '',
'resourceName' => $error->getResourceName(),
'resourceGroup' => $error->getResourceGroup()
]);
}
}
if ($source) {
$source->error();
foreach ($source->getErrors() as $error) {
/** @var MigrationException $error */
call_user_func($this->logError, $error, 'appwrite-worker', 'appwrite-queue-' . self::getName(), [
'migrationId' => $migration->getId(),
'source' => $migration->getAttribute('source') ?? '',
'destination' => $migration->getAttribute('destination') ?? '',
'resourceName' => $error->getResourceName(),
'resourceGroup' => $error->getResourceGroup()
]);
}
}
}
if ($migration->getAttribute('status', '') === 'completed') {
$destination->success();
$source->success();
$destination?->success();
$source?->success();
}
}
}
+18 -6
View File
@@ -15,9 +15,10 @@ class Usage extends Action
{
private array $stats = [];
private int $lastTriggeredTime = 0;
private int $aggregationInterval = 20;
private int $keys = 0;
private const INFINITY_PERIOD = '_inf_';
private const KEYS_THRESHOLD = 10000;
private const KEYS_THRESHOLD = 20000;
public static function getName(): string
{
@@ -39,6 +40,7 @@ class Usage extends Action
$this->action($message, $getProjectDB, $queueForUsageDump);
});
$this->aggregationInterval = (int) System::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', '20');
$this->lastTriggeredTime = time();
}
@@ -56,10 +58,15 @@ class Usage extends Action
if (empty($payload)) {
throw new Exception('Missing payload');
}
//Todo Figure out way to preserve keys when the container is being recreated @shimonewman
$aggregationInterval = (int) System::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', '20');
$project = new Document($payload['project'] ?? []);
$document = $payload['project'] ?? [];
$project = new Document($document);
if (empty($project->getAttribute('database'))) {
var_dump($payload);
return;
}
$projectId = $project->getInternalId();
foreach ($payload['reduce'] ?? [] as $document) {
if (empty($document)) {
@@ -74,7 +81,12 @@ class Usage extends Action
);
}
$this->stats[$projectId]['project'] = $project;
$this->stats[$projectId]['project'] = [
'$id' => $project->getId(),
'$internalId' => $project->getInternalId(),
'database' => $project->getAttribute('database'),
];
$this->stats[$projectId]['receivedAt'] = DateTime::now();
foreach ($payload['metrics'] ?? [] as $metric) {
$this->keys++;
@@ -89,7 +101,7 @@ class Usage extends Action
// If keys crossed threshold or X time passed since the last send and there are some keys in the array ($this->stats)
if (
$this->keys >= self::KEYS_THRESHOLD ||
(time() - $this->lastTriggeredTime > $aggregationInterval && $this->keys > 0)
(time() - $this->lastTriggeredTime > $this->aggregationInterval && $this->keys > 0)
) {
Console::warning('[' . DateTime::now() . '] Aggregated ' . $this->keys . ' keys');
+22 -3
View File
@@ -57,16 +57,35 @@ class UsageDump extends Action
throw new Exception('Missing payload');
}
// TODO: rename both usage workers @shimonewman
foreach ($payload['stats'] ?? [] as $stats) {
$project = new Document($stats['project'] ?? []);
//$project = new Document($stats['project'] ?? []);
/**
* Start temp bug fallback
*/
$document = $stats['project'] ?? [];
if (!empty($document['$uid'])) {
$document['$id'] = $document['$uid'];
}
$project = new Document($document);
if (empty($project->getAttribute('database'))) {
continue;
}
/**
* End temp bug fallback
*/
$numberOfKeys = !empty($stats['keys']) ? count($stats['keys']) : 0;
$receivedAt = $stats['receivedAt'] ?? 'NONE';
if ($numberOfKeys === 0) {
continue;
}
console::log('[' . DateTime::now() . '] ProjectId [' . $project->getInternalId() . '] ReceivedAt [' . $receivedAt . '] ' . $numberOfKeys . ' keys');
console::log('['.DateTime::now().'] Id: '.$project->getId(). ' InternalId: '.$project->getInternalId(). ' Db: '.$project->getAttribute('database').' ReceivedAt: '.$receivedAt. ' Keys: '.$numberOfKeys);
try {
$dbForProject = $getProjectDB($project);
+36 -19
View File
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Event\Mail;
use Appwrite\Event\Usage;
use Appwrite\Template\Template;
use Exception;
use Utopia\Database\Database;
@@ -31,21 +32,22 @@ class Webhooks extends Action
$this
->desc('Webhooks worker')
->inject('message')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForMails')
->inject('queueForUsage')
->inject('log')
->callback(fn (Message $message, Database $dbForConsole, Mail $queueForMails, Log $log) => $this->action($message, $dbForConsole, $queueForMails, $log));
->callback(fn (Message $message, Database $dbForPlatform, Mail $queueForMails, Usage $queueForUsage, Log $log) => $this->action($message, $dbForPlatform, $queueForMails, $queueForUsage, $log));
}
/**
* @param Message $message
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Mail $queueForMails
* @param Log $log
* @return void
* @throws Exception
*/
public function action(Message $message, Database $dbForConsole, Mail $queueForMails, Log $log): void
public function action(Message $message, Database $dbForPlatform, Mail $queueForMails, Usage $queueForUsage, Log $log): void
{
$this->errors = [];
$payload = $message->getPayload() ?? [];
@@ -56,14 +58,15 @@ class Webhooks extends Action
$events = $payload['events'];
$webhookPayload = json_encode($payload['payload']);
$project = new Document($payload['project']);
$user = new Document($payload['user'] ?? []);
$project = new Document($payload['project']);
$project = $dbForPlatform->getDocument('projects', $project->getId());
$log->addTag('projectId', $project->getId());
foreach ($project->getAttribute('webhooks', []) as $webhook) {
if (array_intersect($webhook->getAttribute('events', []), $events)) {
$this->execute($events, $webhookPayload, $webhook, $user, $project, $dbForConsole, $queueForMails);
$this->execute($events, $webhookPayload, $webhook, $user, $project, $dbForPlatform, $queueForMails, $queueForUsage);
}
}
@@ -78,11 +81,11 @@ class Webhooks extends Action
* @param Document $webhook
* @param Document $user
* @param Document $project
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Mail $queueForMails
* @return void
*/
private function execute(array $events, string $payload, Document $webhook, Document $user, Document $project, Database $dbForConsole, Mail $queueForMails): void
private function execute(array $events, string $payload, Document $webhook, Document $user, Document $project, Database $dbForPlatform, Mail $queueForMails, Usage $queueForUsage): void
{
if ($webhook->getAttribute('enabled') !== true) {
return;
@@ -138,8 +141,8 @@ class Webhooks extends Action
\curl_close($ch);
if (!empty($curlError) || $statusCode >= 400) {
$dbForConsole->increaseDocumentAttribute('webhooks', $webhook->getId(), 'attempts', 1);
$webhook = $dbForConsole->getDocument('webhooks', $webhook->getId());
$dbForPlatform->increaseDocumentAttribute('webhooks', $webhook->getId(), 'attempts', 1);
$webhook = $dbForPlatform->getDocument('webhooks', $webhook->getId());
$attempts = $webhook->getAttribute('attempts');
$logs = '';
@@ -158,18 +161,32 @@ class Webhooks extends Action
if ($attempts >= \intval(System::getEnv('_APP_WEBHOOK_MAX_FAILED_ATTEMPTS', '10'))) {
$webhook->setAttribute('enabled', false);
$this->sendEmailAlert($attempts, $statusCode, $webhook, $project, $dbForConsole, $queueForMails);
$this->sendEmailAlert($attempts, $statusCode, $webhook, $project, $dbForPlatform, $queueForMails);
}
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
$dbForConsole->purgeCachedDocument('projects', $project->getId());
$dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$this->errors[] = $logs;
$queueForUsage
->addMetric(METRIC_WEBHOOKS_FAILED, 1)
->addMetric(str_replace('{webhookInternalId}', $webhook->getInternalId(), METRIC_WEBHOOK_ID_FAILED), 1)
;
} else {
$webhook->setAttribute('attempts', 0); // Reset attempts on success
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
$dbForConsole->purgeCachedDocument('projects', $project->getId());
$dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$queueForUsage
->addMetric(METRIC_WEBHOOKS_SENT, 1)
->addMetric(str_replace('{webhookInternalId}', $webhook->getInternalId(), METRIC_WEBHOOK_ID_SENT), 1)
;
}
$queueForUsage
->setProject($project)
->trigger();
}
/**
@@ -177,20 +194,20 @@ class Webhooks extends Action
* @param mixed $statusCode
* @param Document $webhook
* @param Document $project
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Mail $queueForMails
* @return void
*/
public function sendEmailAlert(int $attempts, mixed $statusCode, Document $webhook, Document $project, Database $dbForConsole, Mail $queueForMails): void
public function sendEmailAlert(int $attempts, mixed $statusCode, Document $webhook, Document $project, Database $dbForPlatform, Mail $queueForMails): void
{
$memberships = $dbForConsole->find('memberships', [
$memberships = $dbForPlatform->find('memberships', [
Query::equal('teamInternalId', [$project->getAttribute('teamInternalId')]),
Query::limit(APP_LIMIT_SUBQUERY)
]);
$userIds = array_column(\array_map(fn ($membership) => $membership->getArrayCopy(), $memberships), 'userId');
$users = $dbForConsole->find('users', [
$users = $dbForPlatform->find('users', [
Query::equal('$id', $userIds),
]);
@@ -323,6 +323,7 @@ class OpenAPI3 extends Format
case 'Utopia\Validator\JSON':
case 'Utopia\Validator\Mock':
case 'Utopia\Validator\Assoc':
case 'Appwrite\Functions\Validator\Payload':
$param['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
$node['schema']['type'] = 'object';
$node['schema']['x-example'] = '{}';
@@ -349,6 +349,7 @@ class Swagger2 extends Format
case 'Utopia\Validator\JSON':
case 'Utopia\Validator\Mock':
case 'Utopia\Validator\Assoc':
case 'Appwrite\Functions\Validator\Payload':
$node['type'] = 'object';
$node['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
$node['x-example'] = '{}';
+1 -1
View File
@@ -94,7 +94,7 @@ class Func extends Model
])
->addRule('schedule', [
'type' => self::TYPE_STRING,
'description' => 'Function execution schedult in CRON format.',
'description' => 'Function execution schedule in CRON format.',
'default' => '',
'example' => '5 4 * * *',
])
@@ -1362,7 +1362,7 @@ class DatabasesCustomServerTest extends Scope
]);
$this->assertEquals(400, $tooWide['headers']['status-code']);
$this->assertEquals('Attribute limit exceeded', $tooWide['body']['message']);
$this->assertEquals('attribute_limit_exceeded', $tooWide['body']['type']);
}
public function testIndexLimitException()
@@ -3727,11 +3727,11 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'teamId' => ID::unique(),
'name' => 'Amating Team',
'name' => 'Amazing Team',
]);
$this->assertEquals(201, $team['headers']['status-code']);
$this->assertEquals('Amating Team', $team['body']['name']);
$this->assertEquals('Amazing Team', $team['body']['name']);
$this->assertNotEmpty($team['body']['$id']);
$teamId = $team['body']['$id'];
@@ -3794,6 +3794,115 @@ class ProjectsConsoleClientTest extends Scope
return $data;
}
public function testDeleteSharedProject(): void
{
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'teamId' => ID::unique(),
'name' => 'Amazing Team',
]);
$teamId = $team['body']['$id'];
// Ensure deleting one project does not affect another project
$project1 = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Amazing Project 1',
'teamId' => $teamId,
'region' => 'default'
]);
$project2 = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Amazing Project 2',
'teamId' => $teamId,
'region' => 'default'
]);
$project1Id = $project1['body']['$id'];
$project2Id = $project2['body']['$id'];
// Create user in each project
$key1 = $this->client->call(Client::METHOD_POST, '/projects/' . $project1Id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Key Test',
'scopes' => ['users.read', 'users.write'],
]);
$user1 = $this->client->call(Client::METHOD_POST, '/users', [
'content-type' => 'application/json',
'x-appwrite-project' => $project1Id,
'x-appwrite-key' => $key1['body']['secret'],
], [
'userId' => ID::unique(),
'email' => 'test1@appwrite.io',
'password' => 'password',
]);
$this->assertEquals(201, $user1['headers']['status-code']);
$key2 = $this->client->call(Client::METHOD_POST, '/projects/' . $project2Id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Key Test',
'scopes' => ['users.read', 'users.write'],
]);
$user2 = $this->client->call(Client::METHOD_POST, '/users', [
'content-type' => 'application/json',
'x-appwrite-project' => $project2Id,
'x-appwrite-key' => $key2['body']['secret'],
], [
'userId' => ID::unique(),
'email' => 'test2@appwrite.io',
'password' => 'password',
]);
$this->assertEquals(201, $user2['headers']['status-code']);
// Delete project 1
$project1 = $this->client->call(Client::METHOD_DELETE, '/projects/' . $project1Id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $project1['headers']['status-code']);
\sleep(3);
// Ensure project 2 user is still there
$user2 = $this->client->call(Client::METHOD_GET, '/users/' . $user2['body']['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $project2Id,
'x-appwrite-key' => $key2['body']['secret'],
]);
$this->assertEquals(200, $user2['headers']['status-code']);
// Create another user in project 2 in case read hits stale cache
$user3 = $this->client->call(Client::METHOD_POST, '/users', [
'content-type' => 'application/json',
'x-appwrite-project' => $project2Id,
'x-appwrite-key' => $key2['body']['secret'],
], [
'userId' => ID::unique(),
'email' => 'test3@appwrite.io'
]);
$this->assertEquals(201, $user3['headers']['status-code']);
}
/**
* @depends testCreateProject
*/
@@ -111,6 +111,30 @@ class RealtimeCustomClientTest extends Scope
$client->close();
}
public function testPingPong()
{
$client = $this->getWebsocket(['files'], [
'origin' => 'http://localhost'
]);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('connected', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertCount(1, $response['data']['channels']);
$this->assertContains('files', $response['data']['channels']);
$client->send(\json_encode([
'type' => 'ping'
]));
$response = json_decode($client->receive(), true);
$this->assertEquals('pong', $response['type']);
$client->close();
}
public function testManualAuthentication()
{
$user = $this->getUser();
+72 -2
View File
@@ -559,6 +559,76 @@ trait TeamsBaseClient
return $data;
}
/**
* @depends testCreateTeam
*/
public function testUpdateMembershipWithSession(array $data): void
{
$teamUid = $data['teamUid'] ?? '';
// create user
$response = $this->client->call(Client::METHOD_POST, '/account', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'userId' => 'unique()',
'email' => uniqid() . 'foe@localhost.test',
'password' => 'password',
'name' => 'test'
]);
$this->assertEquals(201, $response['headers']['status-code']);
$user = $response['body'];
// create session
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'email' => $user['email'],
'password' => 'password'
]);
$this->assertEquals(201, $response['headers']['status-code']);
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
$response = $this->client->call(Client::METHOD_POST, '/teams/' . $teamUid . '/memberships', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'email' => $user['email'],
'roles' => ['developer'],
'url' => 'http://localhost:5000/join-us#title'
]);
$this->assertEquals(201, $response['headers']['status-code']);
$lastEmail = $this->getLastEmail();
$secret = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
$membershipUid = substr($lastEmail['text'], strpos($lastEmail['text'], '?membershipId=', 0) + 14, 20);
$userUid = substr($lastEmail['text'], strpos($lastEmail['text'], '&userId=', 0) + 8, 20);
$response = $this->client->call(Client::METHOD_PATCH, '/teams/' . $teamUid . '/memberships/' . $membershipUid . '/status', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
], [
'secret' => $secret,
'userId' => $userUid,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertNotEmpty($response['body']['userId']);
$this->assertNotEmpty($response['body']['teamId']);
$this->assertCount(1, $response['body']['roles']);
$this->assertEmpty($response['cookies']);
}
/**
* @depends testUpdateTeamMembership
*/
@@ -648,7 +718,7 @@ trait TeamsBaseClient
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(3, $response['body']['total']);
$this->assertEquals(4, $response['body']['total']);
$ownerMembershipUid = $response['body']['memberships'][0]['$id'];
@@ -703,7 +773,7 @@ trait TeamsBaseClient
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(2, $response['body']['total']);
$this->assertEquals(3, $response['body']['total']);
/**
* Test for when the owner tries to delete their membership
@@ -83,6 +83,38 @@ class TeamsCustomClientTest extends Scope
$this->assertNotEmpty($response['body']['memberships'][0]['userName']);
$this->assertNotEmpty($response['body']['memberships'][0]['userEmail']);
$this->assertArrayHasKey('mfa', $response['body']['memberships'][0]);
/**
* Update project settings to show only MFA
*/
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $this->getProject()['$id'] . '/auth/memberships-privacy', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'userName' => false,
'userEmail' => false,
'mfa' => true,
]);
$this->assertEquals(200, $response['headers']['status-code']);
/**
* Test that sensitive fields are not shown
*/
$response = $this->client->call(Client::METHOD_GET, '/teams/' . $teamUid . '/memberships', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['total']);
$this->assertNotEmpty($response['body']['memberships'][0]['$id']);
// Assert that sensitive fields are present
$this->assertEmpty($response['body']['memberships'][0]['userName']);
$this->assertEmpty($response['body']['memberships'][0]['userEmail']);
$this->assertArrayHasKey('mfa', $response['body']['memberships'][0]);
}
/**
+8
View File
@@ -310,6 +310,14 @@ trait UsersBase
$this->assertNotEmpty($session['secret']);
$this->assertNotEmpty($session['expire']);
$this->assertEquals('server', $session['provider']);
$response = $this->client->call(Client::METHOD_GET, '/account', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-session' => $session['secret']
]);
$this->assertEquals(200, $response['headers']['status-code']);
}