Compare commits

..
Author SHA1 Message Date
Matej BačoandGitHub a4a589c535 Merge branch '1.8.x' into chore-php-types 2026-01-28 11:44:51 +01:00
HemachandarandGitHub 99dc31062d Fix rule status check (#11195) 2026-01-27 15:36:50 +05:30
Chirag AggarwalandGitHub 6514a992a1 Merge pull request #11078 from appwrite/feat-sdk-for-md 2026-01-26 13:06:10 +05:30
Chirag Aggarwal 1167654c6a Merge branch '1.8.x' into feat-sdk-for-md 2026-01-26 13:03:02 +05:30
Chirag AggarwalandGitHub 28c5df074d Merge pull request #11169 from appwrite/release-cli-13.1.0 2026-01-26 12:59:35 +05:30
Chirag Aggarwal 785efa3355 stable release 2026-01-26 10:40:29 +05:30
Chirag Aggarwal 329b7e1856 Merge branch '1.8.x' into feat-sdk-for-md 2026-01-26 10:25:28 +05:30
Chirag AggarwalandGitHub 0dd2f29a7e Merge pull request #11182 from appwrite/chore-make-update-name-optional 2026-01-23 20:51:36 +05:30
Chirag Aggarwal 67e2104e98 chore: make name update optional in databases and tables 2026-01-23 19:55:06 +05:30
HemachandarandGitHub a2ff833efe Upgrade utopia-php/domains (#11181)
* Upgrade utopia-php/domains

* update
2026-01-23 15:31:20 +05:30
Chirag Aggarwal e894eca201 add rc3 2026-01-23 15:25:05 +05:30
Chirag AggarwalandGitHub 7573e0fee9 Merge branch '1.8.x' into release-cli-13.1.0 2026-01-23 13:42:30 +05:30
Chirag AggarwalandGitHub 46be2d0e49 Merge pull request #11179 from appwrite/fix-deployment-enum 2026-01-23 11:03:54 +05:30
Chirag AggarwalandGitHub 700ed9f882 Merge pull request #11180 from appwrite/chore-update-executor-healthcheck 2026-01-23 11:03:41 +05:30
Chirag Aggarwal 440be52405 chore: update executor healthcheck to improve flakiness 2026-01-23 09:47:54 +05:30
Chirag Aggarwal 7643196d97 fix: deploymentstatus enum missing canceled value 2026-01-23 09:22:18 +05:30
Chirag Aggarwal 7e994f8db5 release cli sdk 13.1.0 2026-01-23 09:15:13 +05:30
Matej Bačo b317f85fb6 Fix depricated schema 2026-01-21 16:27:09 +01:00
Matej Bačo 8e98c08a23 Fix failing tests 2026-01-21 16:05:43 +01:00
Matej Bačo 6ebc36d0f3 Fix merge conflict 2026-01-21 14:47:18 +01:00
Matej Bačo e5ed7c835a Merge branch '1.8.x' into chore-php-types 2026-01-21 14:43:22 +01:00
Damodar LohaniandGitHub cbb5963b29 Merge pull request #11172 from appwrite/fix-missing-doc
Fix: missing doc
2026-01-21 19:07:42 +05:45
Damodar Lohani f376db7b26 docs: add get-queue-audits reference documentation 2026-01-21 13:14:28 +00:00
Chirag Aggarwal bfd34b3b15 fix validation 2026-01-21 14:01:16 +05:30
Chirag Aggarwal a66551863a update composer 2026-01-21 13:46:46 +05:30
Chirag Aggarwal 33abd7f0ed Merge branch '1.8.x' into feat-sdk-for-md 2026-01-21 13:46:33 +05:30
Chirag AggarwalandGitHub 633e290bb4 Merge pull request #11171 from appwrite/fix-compression-enabled-env 2026-01-21 11:00:59 +05:30
Chirag Aggarwal 9b14a8f08d Add _APP_COMPRESSION_ENABLED env variable to .env and docker-compose
The _APP_COMPRESSION_ENABLED environment variable is used in http.php
but was missing from .env and docker-compose.yml, preventing it from
being configured in containerized deployments.
2026-01-21 10:41:29 +05:30
Chirag AggarwalandGitHub ddbddafbf9 Merge pull request #11161 from appwrite/release-cli-13.0.1 2026-01-19 18:00:59 +05:30
Chirag Aggarwal 86a4bfa74e chore: release cli 13.0.1 2026-01-19 17:58:43 +05:30
Jake BarnbyandGitHub 8124b07860 Merge pull request #11033 from appwrite/add-webhooks-and-functions-events 2026-01-19 22:02:49 +13:00
Jake BarnbyandGitHub a7898b3d5b Merge pull request #11159 from appwrite/feat-graphql-introspection 2026-01-19 21:58:56 +13:00
Jake Barnby 5d24b51421 Allow separately enabling graphql introspection 2026-01-19 19:26:17 +13:00
shimon 0203323b4a Remove 'authorization' injection from Bulk Delete, Update, and Upsert classes 2026-01-18 14:01:35 +02:00
shimon 94e29cff53 Fix typo in Authorization parameter in API action definition 2026-01-18 13:11:05 +02:00
shimon 72def3b2fb Refactor API action parameters to include Authorization dependency 2026-01-18 13:05:43 +02:00
shimon d015a75e81 linter 2026-01-18 12:48:36 +02:00
shimon 1306c85eb5 merge with 1.8x 2026-01-18 11:03:06 +02:00
shimon c3ea66b37a Merge branch '1.8.x' of github.com:appwrite/appwrite into add-webhooks-and-functions-events
# Conflicts:
#	app/controllers/shared/api.php
#	app/init/resources.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php
2026-01-18 11:01:40 +02:00
Damodar LohaniandGitHub 72ce068714 Merge pull request #11057 from appwrite/feat-health-module
Feat: Health module
2026-01-18 07:27:19 +05:45
Chirag AggarwalandGitHub ea76213dd2 Merge pull request #11146 from appwrite/feat-cleanup-stale-executions 2026-01-17 12:44:15 +05:30
Matej BačoandGitHub 12b79363d1 Merge pull request #11148 from appwrite/feat-new-schema-dualwriting
Feat: Support dual-writing for new schema features
2026-01-16 15:38:23 +01:00
Jake BarnbyandGitHub 0723101397 Merge pull request #11005 from appwrite/dat-969 2026-01-17 03:31:50 +13:00
Matej BačoandGitHub d56a3c1534 Apply suggestion from @Meldiron 2026-01-16 14:53:05 +01:00
DarshanandGitHub b86d7ac3fa Merge pull request #11149 from appwrite/clean-on-async 2026-01-16 19:07:17 +05:30
ArnabChatterjee20k b1fab79dc4 updated query logic in array to be of and format 2026-01-16 19:06:55 +05:30
Darshan 2f066a6ba8 add: check. 2026-01-16 18:41:22 +05:30
Darshan e8ca0610ee fix: type 2026-01-16 18:40:37 +05:30
Darshan 15caa27977 lint. 2026-01-16 18:38:40 +05:30
Darshan 5f22022527 fix: async being missed. 2026-01-16 18:33:27 +05:30
ArnabChatterjee20k b9c7c172ad updated query conversion for nested query 2026-01-16 18:18:24 +05:30
Matej Bačo c2bf1e8040 Merge branch '1.8.x' into feat-new-schema-dualwriting 2026-01-16 13:24:02 +01:00
Matej Bačo cda03f63ab Support dual-writing for new schema features 2026-01-16 13:23:46 +01:00
Chirag Aggarwal f5a61fb4d6 feat: add cleanup for stale function executions
Adds a new interval task that marks executions stuck in 'processing'
status for more than 30 minutes as 'failed' with a timeout error.
2026-01-16 17:21:05 +05:30
DarshanandGitHub 429f73eb85 Merge pull request #11147 from appwrite/executions-cleanup 2026-01-16 16:33:39 +05:30
ArnabChatterjee20k da871635d9 Fix namespace import for RuntimeQuery class and update test file accordingly 2026-01-16 16:16:03 +05:30
Darshan ccaea5d010 add: constant. 2026-01-16 16:11:38 +05:30
Darshan b5e9c1786a fix: maintenance logic. 2026-01-16 16:04:54 +05:30
Darshan 79e150b7b2 fix: type. 2026-01-16 16:00:59 +05:30
Darshan beee5e721e upate: run on maintenance as well. 2026-01-16 15:55:45 +05:30
Darshan 0ef4bf21cc address comments. 2026-01-16 15:48:21 +05:30
ArnabChatterjee20k b2486fcb6c Merge remote-tracking branch 'upstream/1.8.x' into dat-969 2026-01-16 15:47:25 +05:30
Darshan c67b77bca0 update: implement proper logs cleanup! 2026-01-16 15:06:35 +05:30
Matej Bačo 5d5a14bd77 PR review fixes 2026-01-15 16:16:09 +01:00
Matej Bačo 1308706023 Merge branch '1.8.x' into chore-php-types 2026-01-15 15:52:51 +01:00
Damodar LohaniandGitHub ce91b1a03d Merge pull request #11143 from appwrite/fix-phone-auth-limit
Fix: auth phone limit
2026-01-15 19:43:54 +05:45
Damodar Lohani fbf390c710 Merge remote-tracking branch 'origin/1.8.x' into feat-health-module 2026-01-15 13:55:09 +00:00
Damodar Lohani 991f5ff9fd Catch exception 2026-01-15 13:19:34 +00:00
Damodar Lohani e8d8373922 Fix: phone auth limit 2026-01-15 12:59:30 +00:00
DarshanandGitHub 0fee1f0ffd Merge pull request #11142 from appwrite/bump-libs 2026-01-15 14:45:28 +05:30
Darshan eb07e99225 bump: sdk-gen. 2026-01-15 13:48:50 +05:30
DarshanandGitHub 325968c52a Merge pull request #11141 from appwrite/fix-specs-descriptions 2026-01-15 12:08:35 +05:30
Jake BarnbyandGitHub 39e66711c8 Merge pull request #11140 from appwrite/fix-db-init 2026-01-15 19:36:45 +13:00
DarshanandGitHub 42a1c8da38 Merge branch '1.8.x' into fix-specs-descriptions 2026-01-15 12:05:19 +05:30
Darshan 206bd63620 regen: specs. 2026-01-15 12:04:46 +05:30
Darshan 75696a21bf update: remove excluded keys from descriptions. 2026-01-15 11:56:19 +05:30
Chirag AggarwalandGitHub 48866acdb7 Merge pull request #11112 from appwrite/release-cli-13.0.0.rc3 2026-01-15 11:53:00 +05:30
Jake BarnbyandClaude Opus 4.5 b1171c661e Add setDatabase() to all project database instances
This completes the fix for utopia-php/database 4.5.2 which removed
the automatic USE database statement. All Database instances that
create or query project databases now have explicit setDatabase()
calls.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 19:08:25 +13:00
Jake Barnby a0e4e89621 Update lock 2026-01-15 18:45:18 +13:00
Jake BarnbyandClaude Opus 4.5 728ed57df0 Fix database initialization after utopia-php/database 4.5.2 update
The 4.5.2 update removed the automatic USE database statement on init,
requiring explicit setDatabase() calls on all database resources.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 18:43:28 +13:00
Chirag AggarwalandGitHub 629479d275 Merge branch '1.8.x' into release-cli-13.0.0.rc3 2026-01-15 10:59:11 +05:30
Jake BarnbyandGitHub 5c922f89fe Merge pull request #11139 from appwrite/feat-pool-resilience 2026-01-15 17:30:13 +13:00
Jake BarnbyandGitHub 6e292349b9 Merge pull request #11138 from appwrite/fix-general-unknown 2026-01-15 16:57:50 +13:00
Jake Barnby 7b940e3a17 Increase + parameterise ppolmax reconnect + sleep 2026-01-15 16:47:19 +13:00
Jake Barnby 7ab3debb10 Format 2026-01-15 16:37:09 +13:00
Jake Barnby c083e1ce74 Throw AppwriteException so handler can unwrap 2026-01-15 16:31:37 +13:00
Jake BarnbyandGitHub 61e886eed4 Merge pull request #11137 from appwrite/feat-auth-instance 2026-01-15 12:28:02 +13:00
Chirag Aggarwal 71ac9c7264 use stable 2026-01-14 21:37:00 +05:30
Chirag Aggarwal f78d5523ce mMerge branch '1.8.x' into release-cli-13.0.0.rc3 2026-01-14 21:34:40 +05:30
Chirag Aggarwal 955e1bcbe9 use stable 2026-01-14 21:29:58 +05:30
Jake Barnby 09a337aa1b Fix validators 2026-01-15 04:10:57 +13:00
Jake Barnby 2cfb5ecfd9 Reapply "Merge pull request #11130 from appwrite/feat-auth-instance"
This reverts commit 38687bc24e.
2026-01-15 04:08:00 +13:00
Jake Barnby 38687bc24e Revert "Merge pull request #11130 from appwrite/feat-auth-instance"
This reverts commit c12cad80bb, reversing
changes made to 2a17429226.

# Conflicts:
#	composer.lock
2026-01-15 03:48:42 +13:00
Jake BarnbyandGitHub 77b2a57968 Merge pull request #11136 from appwrite/fix-versions 2026-01-15 03:16:37 +13:00
Jake Barnby 223bb7f08a Update lock 2026-01-15 03:07:26 +13:00
Matej BačoandGitHub b73abf69d0 Merge pull request #11135 from appwrite/feat-new-file-params
Feat: Add more file params
2026-01-14 14:17:37 +01:00
Matej BačoandGitHub e76ed98590 Merge branch '1.8.x' into feat-new-file-params 2026-01-14 14:17:27 +01:00
Matej Bačo 7b10fe1371 typo fix 2026-01-14 14:17:12 +01:00
Matej Bačo e4abc0e0da Add more file params (encryption, compression) 2026-01-14 13:16:50 +01:00
Chirag AggarwalandGitHub 6478499f1c Merge pull request #11134 from appwrite/fix-execution-status-update 2026-01-14 17:45:30 +05:30
DarshanandGitHub 3bbfbdb063 Merge pull request #11132 from appwrite/fix-sdk-handling 2026-01-14 17:18:59 +05:30
Darshan bc6ecbd22c address comments. 2026-01-14 17:15:33 +05:30
Darshan 4d2f631393 update: exclude the mocks.
update: nice group based blacklisting.
2026-01-14 17:07:33 +05:30
Darshan 479a583ff5 exclude the mocks. 2026-01-14 16:55:47 +05:30
Darshan 08cd610823 exclude the mocks. 2026-01-14 16:47:21 +05:30
Darshan 6cd19bf330 address comment for key values on enums. 2026-01-14 16:42:30 +05:30
Darshan d6d8729983 bump: sdk generator. 2026-01-14 16:36:44 +05:30
Chirag Aggarwal 6c866be9f3 Fix execution status not updating if usage stats trigger fails
Move the execution document update inside the finally block and wrap
it in try-catch to ensure the execution record is always updated,
even if queueForStatsUsage->trigger() throws an exception.
2026-01-14 16:12:51 +05:30
Darshan 0ec515779e update: address comments.
regen: specs.
2026-01-14 15:29:30 +05:30
Darshan 91240e0ca7 update: address comments.
regen: specs.
2026-01-14 15:10:42 +05:30
Darshan e3ae7daab4 regen: specs. 2026-01-14 14:58:42 +05:30
Darshan f482e6de68 update: specs generation to exclude mock auth providers. 2026-01-14 14:58:22 +05:30
Jake BarnbyandGitHub c12cad80bb Merge pull request #11130 from appwrite/feat-auth-instance 2026-01-14 20:21:59 +13:00
Chirag AggarwalandGitHub 2a17429226 Merge pull request #11123 from appwrite/feat-integer-format-int64 2026-01-14 12:15:20 +05:30
Jake Barnby e2627d784b Fix router param 2026-01-14 19:27:51 +13:00
Jake Barnby 5c915ef92f Reapply "Merge pull request #11099 from appwrite/feat-auth-instance"
This reverts commit 321fc8ee70.
2026-01-14 19:07:49 +13:00
Chirag AggarwalandGitHub 5697d983d0 Merge branch '1.8.x' into feat-integer-format-int64 2026-01-14 09:54:19 +05:30
Damodar LohaniandGitHub a5fb10c61d Merge pull request #11129 from appwrite/feat-deployment-success
After deployment success hook
2026-01-14 07:15:50 +05:45
Damodar Lohani 5469b78367 feat: add afterDeploymentSuccess hook to handle post-deployment actions 2026-01-14 00:59:22 +00:00
Damodar Lohani e887859cc0 feat: implement afterDeploymentSuccess hook in builds worker and invoke it post-deployment 2026-01-14 00:57:40 +00:00
Chirag Aggarwal 942cfc4591 Merge branch '1.8.x' into feat-integer-format-int64 2026-01-13 21:26:57 +05:30
Jake Barnby 321fc8ee70 Revert "Merge pull request #11099 from appwrite/feat-auth-instance"
This reverts commit a4734a5de7, reversing
changes made to 15922fb88c.

# Conflicts:
#	composer.lock
2026-01-14 02:37:17 +13:00
DarshanandGitHub 568ac54d3d Merge pull request #11122 from appwrite/bump-flutter-dart 2026-01-13 18:09:08 +05:30
Darshan 432e86ec07 regen specs. 2026-01-13 18:04:14 +05:30
Chirag AggarwalandGitHub fef1b5b5e5 Merge pull request #11125 from appwrite/update-domains-lib 2026-01-13 17:35:29 +05:30
Chirag Aggarwal bfac0f92af chore: update domains lib 2026-01-13 17:11:29 +05:30
Chirag Aggarwal ae6df78020 Add int64 format support for integer attributes
This change adds int64 format specification to integer attribute min/max values in the API response models and updates all OpenAPI/Swagger specifications accordingly. This ensures proper type handling for large integer values that exceed int32 range in client SDKs.

Changes:
- Add 'format: int64' to min/max fields in AttributeInteger and ColumnInteger models
- Regenerate OpenAPI 3.x and Swagger 2.x specs for all platforms (client, console, server)
- Update composer dependencies
2026-01-13 17:00:20 +05:30
Darshan 23d84cf29f update: dart sdk. 2026-01-13 14:21:09 +05:30
Darshan c79019f960 regen: specs. 2026-01-13 13:58:57 +05:30
Chirag AggarwalandGitHub dd78b6dbce Merge branch '1.8.x' into release-cli-13.0.0.rc3 2026-01-13 13:39:27 +05:30
Chirag Aggarwal 81e64afe7a changelog 2026-01-13 13:38:44 +05:30
Chirag Aggarwal 5ef675bcdd release cli sdk 13.0.0 rc5 2026-01-13 13:38:32 +05:30
Jake BarnbyandGitHub a4734a5de7 Merge pull request #11099 from appwrite/feat-auth-instance 2026-01-13 20:35:22 +13:00
Jake Barnby 03812cf7bf Lint 2026-01-13 18:59:55 +13:00
Jake Barnby 515a34c180 Set health adapter auth 2026-01-13 18:52:16 +13:00
Jake Barnby 8e688bad4d Revert ping 2026-01-13 18:36:38 +13:00
Damodar Lohani 4905f7cc48 Merge remote-tracking branch 'origin/1.8.x' into feat-health-module 2026-01-13 05:19:00 +00:00
Jake Barnby 3d27b76051 Fix avatars 2026-01-13 18:05:46 +13:00
Jake Barnby 2d6348bf5d Fix health 2026-01-13 17:22:05 +13:00
Jake Barnby 2131a0cea1 Sync auth 2026-01-13 16:21:43 +13:00
Jake Barnby 9379e4d6c8 Merge remote-tracking branch 'origin/1.8.x' into feat-auth-instance
# Conflicts:
#	app/controllers/api/avatars.php
#	composer.lock
#	src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
2026-01-13 15:46:23 +13:00
Jake BarnbyandGitHub 15922fb88c Merge pull request #11103 from appwrite/avatars-module 2026-01-12 21:28:56 +13:00
Jake BarnbyandGitHub ba9526cce0 Merge pull request #11118 from appwrite/bump-http 2026-01-12 21:16:15 +13:00
Darshan 6dbed494e3 bump: swoole and framework. 2026-01-12 13:29:50 +05:30
Darshan 4eb1e7ae45 Merge branch '1.8.x' into avatars-module 2026-01-12 12:04:56 +05:30
Darshan bdd53212a0 Merge branch '1.8.x' into avatars-module 2026-01-12 12:04:41 +05:30
Darshan 1a69d9d236 remove: avatars controller. 2026-01-12 12:03:58 +05:30
Jake BarnbyandGitHub a860c33969 Merge pull request #11115 from appwrite/delete-subscribers 2026-01-12 18:50:59 +13:00
shimon ffd73dc70f Merge remote-tracking branch 'origin/add-webhooks-and-functions-events' into add-webhooks-and-functions-events 2026-01-11 14:12:22 +02:00
shimon 362a7bef0e Merge branch '1.8.x' of github.com:appwrite/appwrite into add-webhooks-and-functions-events 2026-01-11 14:11:51 +02:00
fogelito d08a6f572c message 2026-01-11 08:30:52 +02:00
fogelito 893c1aa669 formatting 2026-01-11 08:21:30 +02:00
fogelito c34873a8b0 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into delete-subscribers 2026-01-11 08:17:40 +02:00
fogelito 17f60b611b catch 2026-01-11 08:13:39 +02:00
Matej Bačo cd0d609229 quality improv 2026-01-10 17:01:31 +01:00
Matej Bačo 497e5f8d00 tests fixes 2026-01-10 16:57:23 +01:00
Matej Bačo 60e1efb8cb Merge conflict fix 2026-01-10 16:42:45 +01:00
Matej Bačo 3d4b29e0f1 Merge branch '1.8.x' into chore-php-types 2026-01-10 16:42:38 +01:00
Matej Bačo dad21a912e PR review changes 2026-01-10 16:35:09 +01:00
Matej BačoandGitHub 445d7495a3 Merge pull request #11110 from appwrite/feat-async-screenshots
Feat: Async screenshots
2026-01-09 14:35:20 +01:00
Matej Bačo aebbe91c34 formatting fix 2026-01-09 11:25:47 +01:00
Matej Bačo 2a9f9f6851 Fix race condition with screenshot worker updating dpeloyment 2026-01-09 11:25:39 +01:00
Matej Bačo eec023aab3 Simplify screenshot failures 2026-01-09 11:25:25 +01:00
Matej Bačo 71f389e1c0 AI PR review 2026-01-09 10:41:58 +01:00
Matej Bačo d71b289025 Implement screenshot worker 2026-01-08 16:51:04 +01:00
Damodar Lohani f4da9b54e7 improve get certificate 2026-01-08 06:34:11 +00:00
Damodar LohaniandGitHub df84b94df0 Merge branch '1.8.x' into feat-health-module 2026-01-08 12:08:57 +05:45
Damodar Lohani 42bf515c8a Fix typos 2026-01-08 06:21:57 +00:00
Jake BarnbyandGitHub a7d39b5ef7 Merge pull request #11105 from appwrite/bump-migrations 2026-01-08 19:15:29 +13:00
Jake Barnby 4c1417944d Fix bucket stats auth 2026-01-08 18:54:52 +13:00
Damodar LohaniandGitHub 27aee1fb88 Merge pull request #11054 from appwrite/feat-file-create-after-success-hook
Feat: add after success hook for file creation endpoint
2026-01-08 11:31:14 +05:45
Darshan 4319c16584 bump: migrations. 2026-01-08 11:13:16 +05:30
Damodar LohaniandGitHub d68fc9a22e Merge branch '1.8.x' into feat-health-module 2026-01-08 11:26:56 +05:45
Jake Barnby e3d6fc123f Update lock 2026-01-08 18:11:37 +13:00
Jake Barnby e6505b9cf9 Merge remote-tracking branch 'origin/1.8.x' into feat-auth-instance
# Conflicts:
#	composer.lock
2026-01-08 17:48:37 +13:00
Jake Barnby c1ab9da740 Update migrations 2026-01-08 17:48:20 +13:00
Jake Barnby b567cd5341 Update storage module for auth instance 2026-01-08 15:01:30 +13:00
Luke B. SilverandGitHub f2ac01bccf Merge pull request #11104 from appwrite/feat-graceful-workers
feat: graceful workers
2026-01-07 23:23:44 +00:00
loks0n eecfba2a72 feat: graceful workers 2026-01-07 16:50:53 +00:00
shimon 2cfaa2223e feat: inject EventProcessor into Update transaction for enhanced event handling 2026-01-07 18:19:20 +02:00
shimon 1a87bde88e removing blank lines 2026-01-07 17:25:12 +02:00
shimon 4b92e3781f removing blank lines 2026-01-07 17:24:53 +02:00
shimon 65bb8946f6 Merge branch '1.8.x' of github.com:appwrite/appwrite into add-webhooks-and-functions-events 2026-01-07 16:58:23 +02:00
shimon db4dcd164e refactor: integrate EventProcessor for handling function and webhook events; streamline event triggering in database actions 2026-01-07 16:57:57 +02:00
Darshan 384c47f436 Merge remote-tracking branch 'origin/avatars-module' into avatars-module 2026-01-07 19:42:56 +05:30
Darshan dcc926402d fix: path error. 2026-01-07 19:42:50 +05:30
Darshan 6969e446d5 Merge branch '1.8.x' into avatars-module 2026-01-07 19:41:49 +05:30
DarshanandGitHub c8bb939357 Merge branch '1.8.x' into avatars-module 2026-01-07 19:37:28 +05:30
Darshan 02488c853d fix: paths error. 2026-01-07 19:28:44 +05:30
Darshan 92f26c1b5a update: move avatars controller to module structure. 2026-01-07 19:09:56 +05:30
DarshanandGitHub ded06d7435 Merge pull request #11102 from appwrite/add-xlist-totalsize 2026-01-07 18:53:53 +05:30
Matej BačoandGitHub 6c59cef8f4 Merge pull request #11090 from appwrite/fix-project-search
Fix: Allows query search on project
2026-01-07 14:09:56 +01:00
Darshan 46072bb95e fix: test. 2026-01-07 18:32:03 +05:30
Darshan 904b7312d6 address comments. 2026-01-07 18:22:36 +05:30
DarshanandGitHub 68fc6b0600 Merge branch '1.8.x' into add-xlist-totalsize 2026-01-07 18:08:59 +05:30
Darshan 47ea8699d1 add: tests. 2026-01-07 18:05:33 +05:30
Darshan 9ba3f6dfe8 add: totalsize on xlist. 2026-01-07 18:00:04 +05:30
HemachandarandGitHub 3483daef0d Stop publishing rule verification errors to Sentry (#11101) 2026-01-07 17:33:00 +05:30
DarshanandGitHub 9943305640 Merge pull request #11100 from appwrite/fix-dat-1019 2026-01-07 16:40:58 +05:30
DarshanandGitHub 8bb6f01e22 Merge pull request #11069 from appwrite/storage-size 2026-01-07 16:36:06 +05:30
Darshan 2cc7bbc0a4 update: address comment, inline method. 2026-01-07 16:00:52 +05:30
Jake Barnby e9366ac2e6 Merge remote-tracking branch 'origin/1.8.x' into feat-auth-instance
# Conflicts:
#	composer.json
#	composer.lock
#	src/Appwrite/Platform/Workers/Migrations.php
2026-01-07 23:26:41 +13:00
Jake Barnby 22f8a3eab9 Sync merge 2026-01-07 23:24:49 +13:00
Darshan 896e5a517a lint. 2026-01-07 15:42:51 +05:30
Darshan 281dcfc64a add queries to logging. 2026-01-07 15:37:56 +05:30
DarshanandGitHub 457cd53475 Merge branch '1.8.x' into storage-size 2026-01-07 14:00:36 +05:30
Darshan 93e7dc772d Merge remote-tracking branch 'origin/storage-size' into storage-size 2026-01-07 13:57:59 +05:30
Darshan 228d095ee5 address comment and fix tests. 2026-01-07 13:57:41 +05:30
Jake BarnbyandGitHub 43189f8940 Merge pull request #11086 from appwrite/migrations-cleanup 2026-01-07 21:15:38 +13:00
DarshanandGitHub b615e4b901 Merge branch '1.8.x' into storage-size 2026-01-07 13:43:48 +05:30
Darshan ebd6457361 address comment. 2026-01-07 13:39:51 +05:30
fogelito 0d3bcc9b3a message todo 2026-01-07 09:52:23 +02:00
fogelito 61e98a501a composer migration 1.3.* 2026-01-07 09:43:13 +02:00
fogelito 502012ddf7 composer migration 1.3.* 2026-01-07 09:42:21 +02:00
fogelito a6eb479c93 composer migration 2026-01-07 09:38:58 +02:00
fogelito e0d7b418dc Merge branch '1.8.x' of https://github.com/appwrite/appwrite into migrations-cleanup
# Conflicts:
#	composer.lock
2026-01-07 09:37:48 +02:00
Jake Barnby ea97072479 Merge remote-tracking branch 'origin/1.8.x' into feat-auth-instance
# Conflicts:
#	composer.lock
2026-01-07 20:10:26 +13:00
Jake Barnby 7573ee75a2 Use authorization instance 2026-01-07 20:04:28 +13:00
Damodar LohaniandGitHub 0dc00eb2c5 Merge pull request #11095 from appwrite/lohanidamodar-patch-2
Fix deleteAuditLogs function call parameters
2026-01-07 12:41:43 +05:45
Jake BarnbyandGitHub b8ccc3b846 Merge pull request #11096 from appwrite/bump-migrations-lib 2026-01-07 19:18:52 +13:00
Damodar LohaniandGitHub 6f1cac7d76 Merge branch '1.8.x' into feat-health-module 2026-01-07 11:53:34 +05:45
Darshan 5f3384b821 bump. 2026-01-07 11:24:15 +05:30
Darshan 71cb462f49 Merge branch '1.8.x' into 'bump-migrations-lib'. 2026-01-07 11:20:00 +05:30
Darshan 27e859030d use: vcs for now. 2026-01-07 11:15:28 +05:30
Damodar LohaniandGitHub e7a82e4d31 Fix deleteAuditLogs function call parameters 2026-01-07 11:08:26 +05:45
Jake BarnbyandGitHub 6dc836307e Merge pull request #11088 from appwrite/delete-project-skip-validations 2026-01-07 18:10:44 +13:00
shimon 0582cdf394 refactor: clean up whitespace and remove commented-out abuse handling code in api.php 2026-01-06 18:41:06 +02:00
shimon 573d8423a3 refactor: remove unused purgeFunctionEventsCache method and clean up whitespace in Update class 2026-01-06 18:40:17 +02:00
shimon 3fa3318346 Merge branch '1.8.x' of github.com:appwrite/appwrite into add-webhooks-and-functions-events 2026-01-06 18:32:00 +02:00
shimon 23dfb23a3b fix: revert Traefik image version to 2.11; implement caching for function events and webhooks; add cache purging on function create/update/delete events 2026-01-06 18:28:37 +02:00
fogelito 5642983f91 Pull main 2026-01-06 17:26:30 +02:00
fogelito b208e5c066 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into migrations-cleanup
# Conflicts:
#	composer.json
#	composer.lock
2026-01-06 17:26:08 +02:00
fogelito 6756ee31b6 migration can not be empty 2026-01-06 16:55:36 +02:00
fogelito 89c988d73c finally try catch 2026-01-06 16:36:34 +02:00
Matej Bačo c0f8dee4d4 Allows query search on project 2026-01-06 15:03:43 +01:00
fogelito eb10996517 Add try finally 2026-01-06 15:36:08 +02:00
fogelito 31803f0eb9 message 2026-01-06 15:30:19 +02:00
fogelito ac9214f3c4 revert 2026-01-06 14:57:54 +02:00
fogelito 14347d86a8 try disableValidation 2026-01-06 14:49:19 +02:00
fogelito 1b855d2d41 disables validations 2026-01-06 14:37:46 +02:00
fogelito 59b41c4b52 lock 2026-01-06 11:16:34 +02:00
fogelito b6e8b55994 Throw 2026-01-06 11:01:38 +02:00
fogelito 3a4fb5dd14 throws 2026-01-06 10:56:33 +02:00
fogelito 2a679ead32 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into migrations-cleanup 2026-01-06 10:45:19 +02:00
fogelito 7da9d480d0 cleanUp 2026-01-06 10:43:16 +02:00
Chirag Aggarwal 001d38caf5 feat: sdk for md 2026-01-05 12:53:35 +05:30
shimon cd651dbdb8 chore: update dependencies and fix formatting issues in composer files; change Traefik image version in docker-compose; add debug output in Action.php 2026-01-04 11:35:20 +02:00
shimon 2dfb386b12 Merge branch '1.8.x' of github.com:appwrite/appwrite into add-webhooks-and-functions-events 2026-01-04 10:39:35 +02:00
shimon e9dac6710f Refactor: Remove unused webhook and function event filters, implement caching for function events retrieval 2026-01-04 09:53:29 +02:00
Darshan a5d4f69c6c bump: specs 2026-01-03 15:43:54 +05:30
Darshan f788fc8f8a add: tests. 2026-01-03 15:42:55 +05:30
Darshan 67ef2ab552 add: return bucket actual size. 2026-01-03 15:13:34 +05:30
Damodar Lohani 297fae8f81 refactor tests 2026-01-01 09:54:13 +00:00
Damodar Lohani 3e403194e4 Fix tests 2026-01-01 09:37:10 +00:00
Damodar Lohani 0305790e69 Fix: update health status model to use HEALTH_STATUS_LIST 2026-01-01 07:59:06 +00:00
Damodar Lohani 25435aaa11 Error handling 2026-01-01 07:58:18 +00:00
Damodar Lohani 19895e54e3 Fix: health status returning ping in incorrect unit 2026-01-01 07:37:09 +00:00
Damodar Lohani 4b3fe0b6fe Fix missing brace 2026-01-01 07:35:28 +00:00
Damodar LohaniGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
4ef906b836 Update src/Appwrite/Platform/Modules/Health/Http/Health/Certificate/Get.php
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-01-01 13:15:28 +05:45
Damodar Lohani 28aa4e8a8d refactor and new endpoint test 2026-01-01 05:49:20 +00:00
Damodar Lohani 9c6a6c265a format 2026-01-01 05:45:14 +00:00
Damodar Lohani dc0eb5f7a7 Feat: Health module 2026-01-01 05:45:06 +00:00
Damodar Lohani 03ccca2c35 Add after create success hook in file creation process 2025-12-31 13:05:41 +00:00
Matej Bačo 00b5236dea simplify diff 2025-12-29 18:41:35 +01:00
Matej Bačo df57b59786 Merge branch '1.8.x' into chore-php-types 2025-12-29 13:24:50 +01:00
Matej Bačo eda189dbf1 AI review improvements 2025-12-29 13:24:26 +01:00
Matej Bačo ee911e3df6 Finalize unit key tests 2025-12-29 11:21:49 +01:00
Matej Bačo eb2c616089 Improve key unit tests 2025-12-29 10:47:27 +01:00
Matej Bačo 5f5d9b4fcb Add async key cleanup 2025-12-29 09:04:47 +01:00
Matej Bačo 7c56a76feb self PR review fixes 2025-12-29 08:59:07 +01:00
shimon 6c1f967509 add functionsEvents and webhooksEvents 2025-12-28 18:10:44 +02:00
Matej Bačo 04f660e44b Dedicate project test 2025-12-28 10:06:29 +01:00
Matej Bačo b4c1b96d43 Fix General tests 2025-12-27 19:28:08 +01:00
Matej Bačo 44d8c66e95 Merge branch '1.8.x' into chore-php-types 2025-12-27 19:18:45 +01:00
Matej Bačo 6774de4eef add todo 2025-12-27 19:18:25 +01:00
Matej Bačo ee0f15eed6 QA bug fixing 2025-12-27 19:08:12 +01:00
Matej Bačo c54d1d29a5 Update stats of all key ypes 2025-12-27 18:44:01 +01:00
ArnabChatterjee20k 3b4196735a refactor: simplify query handling in Realtime adapter and enhance error messaging for unsupported queries 2025-12-24 21:02:03 +05:30
ArnabChatterjee20k 7e315f79cc refactor: improve query handling in Realtime adapter and update RuntimeQuery filter logic 2025-12-24 20:50:05 +05:30
ArnabChatterjee20k 336bd44826 fixed payload in adapter 2025-12-24 20:10:00 +05:30
ArnabChatterjee20k 881d96a653 linting 2025-12-24 19:06:34 +05:30
ArnabChatterjee20k 874e5f61f0 Merge remote-tracking branch 'upstream/1.8.x' into dat-969 2025-12-24 19:01:13 +05:30
ArnabChatterjee20k 39cf207df9 re 2025-12-24 18:57:58 +05:30
Matej Bačo c08acedf6a Fix key test 2025-12-23 15:12:41 +01:00
Matej Bačo 9477a5d980 Fix extensability of collections 2025-12-23 13:30:43 +01:00
Matej Bačo c0c1d693c2 DB schema update for keys 2025-12-23 13:06:25 +01:00
Matej Bačo 6e47fb6c70 Implement auth for organization and account keys 2025-12-23 13:06:19 +01:00
Matej Bačo cca49f8f6a Improve docs 2025-12-23 12:11:01 +01:00
Matej Bačo b0bd9e5b78 Fix security requirement 2025-12-23 12:09:07 +01:00
Matej Bačo 6f7cca7d68 Allow key header for account keys 2025-12-23 12:06:30 +01:00
Matej Bačo bce3ce85d5 account key support 2025-12-23 11:59:38 +01:00
ArnabChatterjee20k b7e2606b9f Enhance Realtime functionality with query support and improve tests
- Updated Realtime adapter to handle queries during subscription.
- Added query filtering capabilities in RuntimeQuery class.
- Modified RealtimeBase and RealtimeCustomClientTest to support query parameters in WebSocket connections.
- Improved test coverage for account and database channels with queries.
2025-12-22 18:01:00 +05:30
Matej Bačo 09d71e73af keys list to be public as its reused for more types of keys 2025-12-22 11:37:38 +01:00
Matej Bačo 465912822f Mark reused key response public 2025-12-22 11:32:45 +01:00
Matej Bačo ca43281fa9 Simplify PR 2025-12-20 09:20:39 +01:00
Matej Bačo c69382f29f Fix invalid index 2025-12-19 16:11:43 +01:00
Matej Bačo d45d27b780 Merge branch '1.8.x' into chore-php-types 2025-12-19 16:06:46 +01:00
Matej BačoandGitHub 859d146e85 Apply suggestions from code review 2025-12-19 13:41:18 +01:00
Matej Bačo 69d5ce0f55 Switch over to resource-based key DB structure 2025-12-19 13:40:32 +01:00
Matej Bačo 0a3877a900 New DB schema 2025-12-19 13:09:34 +01:00
Matej Bačo 31b9ff4f05 Merge branch '1.8.x' into chore-php-types 2025-12-19 12:26:51 +01:00
Matej Bačo 1c3f778da9 PR review fix 2025-12-19 12:26:15 +01:00
Matej Bačo 6f16b56f31 Allow Key extensions 2025-12-18 15:55:11 +01:00
Matej Bačo 59f178d634 Improve PHP types for extensability 2025-12-18 13:37:50 +01:00
425 changed files with 27470 additions and 37988 deletions
+3
View File
@@ -2,6 +2,7 @@ _APP_ENV=development
_APP_EDITION=self-hosted
_APP_LOCALE=en
_APP_WORKER_PER_CORE=6
_APP_COMPRESSION_ENABLED=enabled
_APP_COMPRESSION_MIN_SIZE_BYTES=1024
_APP_CONSOLE_WHITELIST_ROOT=disabled
_APP_CONSOLE_WHITELIST_EMAILS=
@@ -102,9 +103,11 @@ _APP_STATS_RESOURCES_INTERVAL=30
_APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000
_APP_MAINTENANCE_RETENTION_SCHEDULES=86400
_APP_INTERVAL_DOMAIN_VERIFICATION=60
_APP_INTERVAL_CLEANUP_STALE_EXECUTIONS=300
_APP_USAGE_STATS=enabled
_APP_LOGGING_CONFIG=
_APP_LOGGING_CONFIG_REALTIME=
_APP_GRAPHQL_INTROSPECTION=enabled
_APP_GRAPHQL_MAX_BATCH_SIZE=10
_APP_GRAPHQL_MAX_COMPLEXITY=250
_APP_GRAPHQL_MAX_DEPTH=4
+2 -3
View File
@@ -77,6 +77,7 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/queue-count-success && \
chmod +x /usr/local/bin/worker-audits && \
chmod +x /usr/local/bin/worker-builds && \
chmod +x /usr/local/bin/worker-screenshots && \
chmod +x /usr/local/bin/worker-certificates && \
chmod +x /usr/local/bin/worker-databases && \
chmod +x /usr/local/bin/worker-deletes && \
@@ -87,9 +88,7 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/worker-webhooks && \
chmod +x /usr/local/bin/worker-stats-usage && \
chmod +x /usr/local/bin/stats-resources && \
chmod +x /usr/local/bin/worker-stats-resources && \
chmod +x /usr/local/bin/worker-payments-usage && \
chmod +x /usr/local/bin/schedule-payments-usage
chmod +x /usr/local/bin/worker-stats-resources
RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/
+21 -14
View File
@@ -5,7 +5,6 @@ require_once __DIR__ . '/init.php';
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Appwrite\Event\Func;
use Appwrite\Event\PaymentsUsage;
use Appwrite\Event\StatsResources;
use Appwrite\Event\StatsUsage;
use Appwrite\Platform\Appwrite;
@@ -42,8 +41,6 @@ Config::setParam('runtimes', (new Runtimes('v5'))->getAll(supported: false));
// require controllers after overwriting runtimes
require_once __DIR__ . '/controllers/general.php';
Authorization::disable();
CLI::setResource('register', fn () => $register);
CLI::setResource('cache', function ($pools) {
@@ -61,7 +58,13 @@ CLI::setResource('pools', function (Registry $register) {
return $register->get('pools');
}, ['register']);
CLI::setResource('dbForPlatform', function ($pools, $cache) {
CLI::setResource('authorization', function () {
$authorization = new Authorization();
$authorization->disable();
return $authorization;
}, []);
CLI::setResource('dbForPlatform', function ($pools, $cache, $authorization) {
$sleep = 3;
$maxAttempts = 5;
$attempts = 0;
@@ -75,6 +78,8 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) {
$dbForPlatform = new Database($adapter, $cache);
$dbForPlatform
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', 'console');
@@ -100,7 +105,7 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) {
}
return $dbForPlatform;
}, ['pools', 'cache']);
}, ['pools', 'cache', 'authorization']);
CLI::setResource('console', function () {
return new Document(Config::getParam('console'));
@@ -111,10 +116,10 @@ CLI::setResource(
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
);
CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases) {
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -147,6 +152,7 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$databases[$dsn->getHost()] = $database;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
@@ -163,17 +169,19 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId());
return $database;
};
}, ['pools', 'dbForPlatform', 'cache']);
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) {
CLI::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, $database) {
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant((int)$project->getSequence());
return $database;
@@ -183,6 +191,8 @@ CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) {
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_TASK)
@@ -195,7 +205,7 @@ CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) {
return $database;
};
}, ['pools', 'cache']);
}, ['pools', 'cache', 'authorization']);
CLI::setResource('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']);
@@ -229,9 +239,6 @@ CLI::setResource('queueForDeletes', function (Publisher $publisher) {
CLI::setResource('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
CLI::setResource('queueForPaymentsUsage', function (Publisher $publisher) {
return new PaymentsUsage($publisher);
}, ['publisher']);
CLI::setResource('logError', function (Registry $register) {
return function (Throwable $error, string $namespace, string $action) use ($register) {
Console::error('[Error] Timestamp: ' . date('c', time()));
+2 -2
View File
@@ -26,8 +26,8 @@ unset($common['files']);
$collections = [
'buckets' => $buckets,
'databases' => $databases,
'projects' => array_merge($projects, $common),
'console' => array_merge($platform, $common),
'projects' => array_merge_recursive($projects, $common),
'console' => array_merge_recursive($platform, $common),
'logs' => $logs,
];
+11
View File
@@ -1288,6 +1288,17 @@ return [
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('labels'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => false,
'default' => null,
'array' => true,
'filters' => [],
],
],
'indexes' => [
[
+10 -26
View File
@@ -232,17 +232,6 @@ $platformCollections = [
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('payments'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 65536,
'signed' => true,
'required' => false,
'default' => [],
'array' => false,
'filters' => ['json', 'encrypt'],
],
[
'$id' => ID::custom('auths'),
'type' => Database::VAR_STRING,
@@ -643,17 +632,19 @@ $platformCollections = [
'$id' => ID::custom('keys'),
'name' => 'keys',
'attributes' => [
// Delete eventuelly, when removing dual-write too
[
'$id' => ID::custom('projectInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
// Delete eventuelly, when removing dual-write too
[
'$id' => ID::custom('projectId'),
'type' => Database::VAR_STRING,
@@ -666,7 +657,7 @@ $platformCollections = [
'filters' => [],
],
[
'$id' => 'resourceType',
'$id' => ID::custom('resourceType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
@@ -677,7 +668,7 @@ $platformCollections = [
'filters' => [],
],
[
'$id' => 'resourceId',
'$id' => ID::custom('resourceId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
@@ -688,7 +679,7 @@ $platformCollections = [
'filters' => [],
],
[
'$id' => 'resourceInternalId',
'$id' => ID::custom('resourceInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
@@ -767,21 +758,14 @@ $platformCollections = [
],
'indexes' => [
[
'$id' => ID::custom('_key_project'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_resource',
'$id' => ID::custom('_key_resource'),
'type' => Database::INDEX_KEY,
'attributes' => ['resourceType', 'resourceInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
'lengths' => [],
'orders' => [],
],
[
'$id' => '_key_accessedAt',
'$id' => ID::custom('_key_accessedAt'),
'type' => Database::INDEX_KEY,
'attributes' => ['accessedAt'],
'lengths' => [],
+106 -121
View File
@@ -567,6 +567,17 @@ return [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('deploymentRetention'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('deploymentInternalId'),
'type' => Database::VAR_STRING,
@@ -765,6 +776,17 @@ return [
'default' => null,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('startCommand'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 20000,
'signed' => true,
'required' => false,
'default' => null,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('specification'),
@@ -776,6 +798,28 @@ return [
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('buildSpecification'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => false,
'required' => false,
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('runtimeSpecification'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => false,
'required' => false,
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'$id' => ID::custom('scopes'),
'type' => Database::VAR_STRING,
@@ -1035,6 +1079,17 @@ return [
'default' => null,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('startCommand'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 20000,
'signed' => true,
'required' => false,
'default' => null,
'filters' => [],
],
[
'$id' => ID::custom('fallbackFile'),
'type' => Database::VAR_STRING,
@@ -1046,6 +1101,17 @@ return [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('deploymentRetention'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('deploymentInternalId'),
'type' => Database::VAR_STRING,
@@ -1200,6 +1266,28 @@ return [
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('buildSpecification'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => false,
'required' => false,
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('runtimeSpecification'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => false,
'required' => false,
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'$id' => ID::custom('buildRuntime'),
'type' => Database::VAR_STRING,
@@ -1357,6 +1445,17 @@ return [
'default' => null,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('startCommand'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 20000,
'signed' => true,
'required' => false,
'default' => null,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('buildOutput'),
@@ -2098,6 +2197,13 @@ return [
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_resourceType'),
'type' => Database::INDEX_KEY,
'attributes' => ['resourceType'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
],
],
@@ -2645,125 +2751,4 @@ return [
],
],
],
'payments_plans' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('payments_plans'),
'name' => 'Payments Plans',
'attributes' => [
[ '$id' => ID::custom('planId'), 'type' => Database::VAR_STRING, 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => true, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('name'), 'type' => Database::VAR_STRING, 'size' => 2048, 'signed' => true, 'required' => false, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('description'), 'type' => Database::VAR_STRING, 'size' => 8192, 'signed' => true, 'required' => false, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('pricing'), 'type' => Database::VAR_STRING, 'size' => 65536, 'signed' => true, 'required' => false, 'default' => [], 'array' => false, 'filters' => ['json'] ],
[ '$id' => ID::custom('isDefault'), 'type' => Database::VAR_BOOLEAN, 'size' => 0, 'signed' => true, 'required' => false, 'default' => false, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('isFree'), 'type' => Database::VAR_BOOLEAN, 'size' => 0, 'signed' => true, 'required' => false, 'default' => false, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('status'), 'type' => Database::VAR_STRING, 'size' => 32, 'signed' => true, 'required' => false, 'default' => 'active', 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('providers'), 'type' => Database::VAR_STRING, 'size' => 65536, 'signed' => true, 'required' => false, 'default' => [], 'array' => false, 'filters' => ['json'] ],
[ '$id' => ID::custom('features'), 'type' => Database::VAR_STRING, 'size' => 65536, 'signed' => true, 'required' => false, 'default' => [], 'array' => false, 'filters' => ['json'] ],
[ '$id' => ID::custom('search'), 'type' => Database::VAR_STRING, 'size' => 16384, 'signed' => true, 'required' => false, 'default' => null, 'array' => false, 'filters' => [] ],
],
'indexes' => [
[ '$id' => ID::custom('_key_plan'), 'type' => Database::INDEX_UNIQUE, 'attributes' => ['planId'], 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC] ],
[ '$id' => ID::custom('_key_status'), 'type' => Database::INDEX_KEY, 'attributes' => ['status'], 'lengths' => [32], 'orders' => [Database::ORDER_ASC] ],
[ '$id' => ID::custom('_fulltext_search'), 'type' => Database::INDEX_FULLTEXT, 'attributes' => ['search'], 'lengths' => [], 'orders' => [] ],
],
],
'payments_features' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('payments_features'),
'name' => 'Payments Features',
'attributes' => [
[ '$id' => ID::custom('featureId'), 'type' => Database::VAR_STRING, 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => true, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('name'), 'type' => Database::VAR_STRING, 'size' => 2048, 'signed' => true, 'required' => false, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('type'), 'type' => Database::VAR_STRING, 'size' => 32, 'signed' => true, 'required' => true, 'default' => 'boolean', 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('description'), 'type' => Database::VAR_STRING, 'size' => 8192, 'signed' => true, 'required' => false, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('providers'), 'type' => Database::VAR_STRING, 'size' => 65536, 'signed' => true, 'required' => false, 'default' => [], 'array' => false, 'filters' => ['json'] ],
],
'indexes' => [
[ '$id' => ID::custom('_key_feature'), 'type' => Database::INDEX_UNIQUE, 'attributes' => ['featureId'], 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC] ],
[ '$id' => ID::custom('_key_type'), 'type' => Database::INDEX_KEY, 'attributes' => ['type'], 'lengths' => [32], 'orders' => [Database::ORDER_ASC] ],
],
],
'payments_plan_features' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('payments_plan_features'),
'name' => 'Payments Plan Features',
'attributes' => [
[ '$id' => ID::custom('planId'), 'type' => Database::VAR_STRING, 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => true, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('featureId'), 'type' => Database::VAR_STRING, 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => true, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('type'), 'type' => Database::VAR_STRING, 'size' => 32, 'signed' => true, 'required' => true, 'default' => 'boolean', 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('enabled'), 'type' => Database::VAR_BOOLEAN, 'size' => 0, 'signed' => true, 'required' => false, 'default' => true, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('currency'), 'type' => Database::VAR_STRING, 'size' => 8, 'signed' => true, 'required' => false, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('interval'), 'type' => Database::VAR_STRING, 'size' => 16, 'signed' => true, 'required' => false, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('includedUnits'), 'type' => Database::VAR_INTEGER, 'size' => 0, 'signed' => false, 'required' => false, 'default' => 0, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('tiersMode'), 'type' => Database::VAR_STRING, 'size' => 16, 'signed' => true, 'required' => false, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('tiers'), 'type' => Database::VAR_STRING, 'size' => 65536, 'signed' => true, 'required' => false, 'default' => [], 'array' => false, 'filters' => ['json'] ],
[ '$id' => ID::custom('usageCap'), 'type' => Database::VAR_INTEGER, 'size' => 0, 'signed' => false, 'required' => false, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('overagePrice'), 'type' => Database::VAR_INTEGER, 'size' => 0, 'signed' => false, 'required' => false, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('providers'), 'type' => Database::VAR_STRING, 'size' => 65536, 'signed' => true, 'required' => false, 'default' => [], 'array' => false, 'filters' => ['json'] ],
[ '$id' => ID::custom('metadata'), 'type' => Database::VAR_STRING, 'size' => 65536, 'signed' => true, 'required' => false, 'default' => [], 'array' => false, 'filters' => ['json'] ],
],
'indexes' => [
[ '$id' => ID::custom('_key_unique'), 'type' => Database::INDEX_UNIQUE, 'attributes' => ['planId','featureId'], 'lengths' => [Database::LENGTH_KEY, Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC, Database::ORDER_ASC] ],
[ '$id' => ID::custom('_key_type'), 'type' => Database::INDEX_KEY, 'attributes' => ['type'], 'lengths' => [16], 'orders' => [Database::ORDER_ASC] ],
],
],
'payments_subscriptions' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('payments_subscriptions'),
'name' => 'Payments Subscriptions',
'attributes' => [
[ '$id' => ID::custom('subscriptionId'), 'type' => Database::VAR_STRING, 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => true, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('providerSubscriptionId'), 'type' => Database::VAR_STRING, 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('providerCheckoutId'), 'type' => Database::VAR_STRING, 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('actorType'), 'type' => Database::VAR_STRING, 'size' => 16, 'signed' => true, 'required' => true, 'default' => 'user', 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('actorId'), 'type' => Database::VAR_STRING, 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => true, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('actorInternalId'), 'type' => Database::VAR_STRING, 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => true, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('planId'), 'type' => Database::VAR_STRING, 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => true, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('priceId'), 'type' => Database::VAR_STRING, 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('status'), 'type' => Database::VAR_STRING, 'size' => 32, 'signed' => true, 'required' => false, 'default' => 'active', 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('trialEndsAt'), 'type' => Database::VAR_DATETIME, 'size' => 0, 'signed' => false, 'required' => false, 'default' => null, 'array' => false, 'filters' => ['datetime'] ],
[ '$id' => ID::custom('currentPeriodStart'), 'type' => Database::VAR_DATETIME, 'size' => 0, 'signed' => false, 'required' => false, 'default' => null, 'array' => false, 'filters' => ['datetime'] ],
[ '$id' => ID::custom('currentPeriodEnd'), 'type' => Database::VAR_DATETIME, 'size' => 0, 'signed' => false, 'required' => false, 'default' => null, 'array' => false, 'filters' => ['datetime'] ],
[ '$id' => ID::custom('cancelAtPeriodEnd'), 'type' => Database::VAR_BOOLEAN, 'size' => 0, 'signed' => true, 'required' => false, 'default' => false, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('canceledAt'), 'type' => Database::VAR_DATETIME, 'size' => 0, 'signed' => false, 'required' => false, 'default' => null, 'array' => false, 'filters' => ['datetime'] ],
[ '$id' => ID::custom('providers'), 'type' => Database::VAR_STRING, 'size' => 65536, 'signed' => true, 'required' => false, 'default' => [], 'array' => false, 'filters' => ['json'] ],
[ '$id' => ID::custom('usageSummary'), 'type' => Database::VAR_STRING, 'size' => 65536, 'signed' => true, 'required' => false, 'default' => [], 'array' => false, 'filters' => ['json'] ],
[ '$id' => ID::custom('tags'), 'type' => Database::VAR_STRING, 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, 'default' => [], 'array' => true, 'filters' => [] ],
[ '$id' => ID::custom('search'), 'type' => Database::VAR_STRING, 'size' => 16384, 'signed' => true, 'required' => false, 'default' => null, 'array' => false, 'filters' => [] ],
],
'indexes' => [
[ '$id' => ID::custom('_key_actor'), 'type' => Database::INDEX_KEY, 'attributes' => ['actorType','actorId'], 'lengths' => [16, Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC, Database::ORDER_ASC] ],
[ '$id' => ID::custom('_key_status'), 'type' => Database::INDEX_KEY, 'attributes' => ['status'], 'lengths' => [32], 'orders' => [Database::ORDER_ASC] ],
[ '$id' => ID::custom('_key_plan'), 'type' => Database::INDEX_KEY, 'attributes' => ['planId'], 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC] ],
[ '$id' => ID::custom('_key_provider_subscription'), 'type' => Database::INDEX_KEY, 'attributes' => ['providerSubscriptionId'], 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC] ],
[ '$id' => ID::custom('_key_provider_checkout'), 'type' => Database::INDEX_KEY, 'attributes' => ['providerCheckoutId'], 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC] ],
[ '$id' => ID::custom('_fulltext_search'), 'type' => Database::INDEX_FULLTEXT, 'attributes' => ['search'], 'lengths' => [], 'orders' => [] ],
],
],
'payments_usage_events' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('payments_usage_events'),
'name' => 'Payments Usage Events',
'attributes' => [
[ '$id' => ID::custom('subscriptionId'), 'type' => Database::VAR_STRING, 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => true, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('actorType'), 'type' => Database::VAR_STRING, 'size' => 16, 'signed' => true, 'required' => true, 'default' => 'user', 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('actorId'), 'type' => Database::VAR_STRING, 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => true, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('planId'), 'type' => Database::VAR_STRING, 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => true, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('featureId'), 'type' => Database::VAR_STRING, 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => true, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('quantity'), 'type' => Database::VAR_INTEGER, 'size' => 0, 'signed' => false, 'required' => true, 'default' => 0, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('timestamp'), 'type' => Database::VAR_DATETIME, 'size' => 0, 'signed' => false, 'required' => true, 'default' => null, 'array' => false, 'filters' => ['datetime'] ],
[ '$id' => ID::custom('providerSyncState'), 'type' => Database::VAR_STRING, 'size' => 32, 'signed' => true, 'required' => false, 'default' => 'pending', 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('providerEventId'), 'type' => Database::VAR_STRING, 'size' => 256, 'signed' => true, 'required' => false, 'default' => null, 'array' => false, 'filters' => [] ],
[ '$id' => ID::custom('metadata'), 'type' => Database::VAR_STRING, 'size' => 65536, 'signed' => true, 'required' => false, 'default' => [], 'array' => false, 'filters' => ['json'] ],
],
'indexes' => [
[ '$id' => ID::custom('_key_subscription_feature_time'), 'type' => Database::INDEX_KEY, 'attributes' => ['subscriptionId','featureId','timestamp'], 'lengths' => [Database::LENGTH_KEY, Database::LENGTH_KEY, 0], 'orders' => [Database::ORDER_ASC, Database::ORDER_ASC, Database::ORDER_ASC] ],
[ '$id' => ID::custom('_key_sync_state'), 'type' => Database::INDEX_KEY, 'attributes' => ['providerSyncState'], 'lengths' => [32], 'orders' => [Database::ORDER_ASC] ],
],
],
];
+22 -40
View File
@@ -357,6 +357,11 @@ return [
'description' => 'API key and session used in the same request. Use either `setSession` or `setKey`. Learn about which authentication method to use in the SSR docs: https://appwrite.io/docs/products/auth/server-side-rendering',
'code' => 403,
],
Exception::USER_JWT_AND_COOKIE_SET => [
'name' => Exception::USER_JWT_AND_COOKIE_SET,
'description' => 'JWT and cookie used in the same request. Use either `setJWT` or `setCookie`. Learn about which authentication method to use in the SSR docs: https://appwrite.io/docs/products/auth/server-side-rendering',
'code' => 403,
],
Exception::API_KEY_EXPIRED => [
'name' => Exception::API_KEY_EXPIRED,
'description' => 'The dynamic API key has expired. Please don\'t use dynamic API keys for more than duration of the execution.',
@@ -1074,6 +1079,11 @@ return [
'description' => 'The project key has expired. Please generate a new key using the Appwrite console.',
'code' => 401,
],
Exception::ACCOUNT_KEY_EXPIRED => [
'name' => Exception::ACCOUNT_KEY_EXPIRED,
'description' => 'The account API key has expired. Please generate a new key using the Appwrite console.',
'code' => 401,
],
Exception::ROUTER_HOST_NOT_FOUND => [
'name' => Exception::ROUTER_HOST_NOT_FOUND,
'description' => 'Host is not trusted. This could occur because you have not configured a custom domain. Add a custom domain to your project first and try again.',
@@ -1103,7 +1113,6 @@ return [
'name' => Exception::RULE_VERIFICATION_FAILED,
'description' => 'Domain verification failed. Please check if your DNS records are correct and try again.',
'code' => 400,
'publish' => true
],
Exception::PROJECT_SMTP_CONFIG_INVALID => [
'name' => Exception::PROJECT_SMTP_CONFIG_INVALID,
@@ -1329,46 +1338,19 @@ return [
'description' => 'Target has an invalid provider type.',
'code' => 400,
],
/** Payments */
Exception::PAYMENT_PLAN_NOT_FOUND => [
'name' => Exception::PAYMENT_PLAN_NOT_FOUND,
'description' => 'Payment plan with the requested ID could not be found.',
'code' => 404,
Exception::USER_ID_MISSING => [
'name' => Exception::USER_ID_MISSING,
'description' => 'When using account API key, make sure to pass x-appwrite-user header with your user ID.',
'code' => 403,
],
Exception::PAYMENT_PLAN_ALREADY_EXISTS => [
'name' => Exception::PAYMENT_PLAN_ALREADY_EXISTS,
'description' => 'Payment plan with the requested ID already exists.',
'code' => 409,
Exception::ORGANIZATION_ID_MISSING => [
'name' => Exception::ORGANIZATION_ID_MISSING,
'description' => 'When using organization API key, make sure to pass x-appwrite-organization header with your organization ID.',
'code' => 403,
],
Exception::PAYMENT_SUBSCRIPTION_NOT_FOUND => [
'name' => Exception::PAYMENT_SUBSCRIPTION_NOT_FOUND,
'description' => 'Payment subscription with the requested ID could not be found.',
'code' => 404,
],
Exception::PAYMENT_SUBSCRIPTION_ALREADY_EXISTS => [
'name' => Exception::PAYMENT_SUBSCRIPTION_ALREADY_EXISTS,
'description' => 'Payment subscription already exists for this actor.',
'code' => 409,
],
Exception::PAYMENT_PROVIDER_NOT_CONFIGURED => [
'name' => Exception::PAYMENT_PROVIDER_NOT_CONFIGURED,
'description' => 'No payment provider has been configured for this project.',
'code' => 400,
],
Exception::PAYMENT_PROVIDER_ALREADY_CONFIGURED => [
'name' => Exception::PAYMENT_PROVIDER_ALREADY_CONFIGURED,
'description' => 'Payment provider is already configured. Disconnect the existing provider before configuring a new one.',
'code' => 409,
],
Exception::PAYMENT_WEBHOOK_FAILED => [
'name' => Exception::PAYMENT_WEBHOOK_FAILED,
'description' => 'Failed to create payment provider webhook.',
'code' => 500,
],
Exception::PAYMENT_FEATURE_NOT_FOUND => [
'name' => Exception::PAYMENT_FEATURE_NOT_FOUND,
'description' => 'Payment feature with the requested ID could not be found.',
'code' => 404,
Exception::PROJECT_ID_MISSING => [
'name' => Exception::PROJECT_ID_MISSING,
'description' => 'When using project API key, make sure to pass x-appwrite-project header with your project ID.',
'code' => 403,
],
];
+15
View File
@@ -0,0 +1,15 @@
<?php
// List of scopes for Account API keys (Tokens)
return [
"account" => [
"description" => 'Access to manage account, its organizations, sessions, tokens, and billing.',
],
"teams.read" => [
"description" => 'Access to read account\'s organizations.',
],
"teams.write" => [
"description" => 'Access to create, update and delete account\'s organizations and its memberships.',
],
];
+42
View File
@@ -0,0 +1,42 @@
<?php
// List of scopes for organization (teams) API keys
return [
"platforms.read" => [
"description" => 'Access to read project\'s platforms',
],
"platforms.write" => [
"description" =>
'Access to create, update, and delete project\'s platforms',
],
"projects.read" => [
"description" => 'Access to read organization\'s projects',
],
"projects.write" => [
"description" =>
"Access to create, update, and delete projects in organization",
],
"keys.read" => [
"description" => 'Access to read project\'s API keys',
],
"keys.write" => [
"description" =>
"Access to create, update, and delete project\'s API keys",
],
"devKeys.read" => [
"description" => 'Access to read project\'s development keys',
],
"devKeys.write" => [
"description" =>
"Access to create, update, and delete project\'s development keys",
],
"webhooks.read" => [
"description" =>
"Access to read project\'s webhooks",
],
"webhooks.write" => [
"description" =>
"Access to create, update, and delete project\'s webhooks",
],
];
@@ -166,13 +166,4 @@ return [ // List of publicly visible scopes
'tokens.write' => [
'description' => 'Access to create, update, and delete your project\'s tokens',
],
'payments.read' => [
'description' => 'Access to read your project\'s payments data (plans, subscriptions, usage)'
],
'payments.write' => [
'description' => 'Access to create, update, and delete your project\'s payments resources'
],
'payments.subscribe' => [
'description' => 'Access to create and manage subscriptions as an actor (user/team)'
],
];
+23 -3
View File
@@ -60,7 +60,7 @@ return [
[
'key' => 'flutter',
'name' => 'Flutter',
'version' => '20.3.2',
'version' => '20.3.3',
'url' => 'https://github.com/appwrite/sdk-for-flutter',
'package' => 'https://pub.dev/packages/appwrite',
'enabled' => true,
@@ -227,7 +227,7 @@ return [
[
'key' => 'cli',
'name' => 'Command Line',
'version' => '12.0.1',
'version' => '13.1.0',
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
@@ -250,6 +250,26 @@ return [
],
],
],
[
'key' => 'markdown',
'name' => 'Markdown',
'version' => '0.1.0',
'url' => 'https://github.com/appwrite/sdk-for-md.git',
'package' => 'https://www.npmjs.com/package/@appwrite.io/docs',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'family' => APP_SDK_PLATFORM_CONSOLE,
'prism' => 'markdown',
'source' => \realpath(__DIR__ . '/../sdks/console-md'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-md.git',
'gitRepoName' => 'sdk-for-md',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'repoBranch' => 'main',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/md/CHANGELOG.md'),
],
],
],
@@ -377,7 +397,7 @@ return [
[
'key' => 'dart',
'name' => 'Dart',
'version' => '20.1.0',
'version' => '20.1.1',
'url' => 'https://github.com/appwrite/sdk-for-dart',
'package' => 'https://pub.dev/packages/dart_appwrite',
'enabled' => true,
+3 -16
View File
@@ -48,7 +48,7 @@ return [
'name' => 'Avatars',
'subtitle' => 'The Avatars service aims to help you complete everyday tasks related to your app image, icons, and avatars.',
'description' => '/docs/services/avatars.md',
'controller' => 'api/avatars.php',
'controller' => '', // Uses modules
'sdk' => true,
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/client/avatars',
@@ -104,7 +104,7 @@ return [
'name' => 'Health',
'subtitle' => 'The Health service allows you to both validate and monitor your Appwrite server\'s health.',
'description' => '/docs/services/health.md',
'controller' => 'api/health.php',
'controller' => '', // Uses modules
'sdk' => true,
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/server/health',
@@ -146,7 +146,7 @@ return [
'name' => 'Storage',
'subtitle' => 'The Storage service allows you to manage your project files.',
'description' => '/docs/services/storage.md',
'controller' => '',
'controller' => '', // Uses modules
'sdk' => true,
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/client/storage',
@@ -225,19 +225,6 @@ return [
'icon' => '/images/services/functions.png',
'platforms' => ['client', 'server', 'console'],
],
'payments' => [
'key' => 'payments',
'name' => 'Payments',
'subtitle' => 'Create and manage plans, subscriptions, usage and billing providers.',
'description' => '/docs/services/payments.md',
'controller' => '', // Uses modules
'sdk' => true,
'docs' => true,
'docsUrl' => '',
'tests' => false,
'optional' => true,
'icon' => '/images/services/databases.png',
],
'proxy' => [
'key' => 'proxy',
'name' => 'Proxy',
+157 -117
View File
@@ -1,7 +1,7 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.8.0",
"version": "1.8.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",
@@ -140,7 +140,8 @@
"email": {
"type": "string",
"description": "User email.",
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"password": {
"type": "string",
@@ -224,12 +225,14 @@
"email": {
"type": "string",
"description": "User email.",
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"password": {
"type": "string",
"description": "User password. Must be at least 8 chars.",
"x-example": "password"
"x-example": "password",
"format": "password"
}
},
"required": [
@@ -439,7 +442,8 @@
"duration": {
"type": "integer",
"description": "Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds.",
"x-example": 0
"x-example": 0,
"format": "int32"
}
}
}
@@ -551,7 +555,7 @@
"x-appwrite": {
"method": "updateMFA",
"group": "mfa",
"weight": 288,
"weight": 253,
"cookies": false,
"type": "",
"demo": "account\/update-mfa.md",
@@ -623,7 +627,7 @@
"x-appwrite": {
"method": "createMfaAuthenticator",
"group": "mfa",
"weight": 290,
"weight": 255,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-authenticator.md",
@@ -747,7 +751,7 @@
"x-appwrite": {
"method": "updateMfaAuthenticator",
"group": "mfa",
"weight": 291,
"weight": 256,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-authenticator.md",
@@ -887,7 +891,7 @@
"x-appwrite": {
"method": "deleteMfaAuthenticator",
"group": "mfa",
"weight": 292,
"weight": 257,
"cookies": false,
"type": "",
"demo": "account\/delete-mfa-authenticator.md",
@@ -1011,7 +1015,7 @@
"x-appwrite": {
"method": "createMfaChallenge",
"group": "mfa",
"weight": 296,
"weight": 261,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-challenge.md",
@@ -1145,7 +1149,7 @@
"x-appwrite": {
"method": "updateMfaChallenge",
"group": "mfa",
"weight": 297,
"weight": 262,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-challenge.md",
@@ -1283,7 +1287,7 @@
"x-appwrite": {
"method": "listMfaFactors",
"group": "mfa",
"weight": 289,
"weight": 254,
"cookies": false,
"type": "",
"demo": "account\/list-mfa-factors.md",
@@ -1384,7 +1388,7 @@
"x-appwrite": {
"method": "getMfaRecoveryCodes",
"group": "mfa",
"weight": 295,
"weight": 260,
"cookies": false,
"type": "",
"demo": "account\/get-mfa-recovery-codes.md",
@@ -1483,7 +1487,7 @@
"x-appwrite": {
"method": "createMfaRecoveryCodes",
"group": "mfa",
"weight": 293,
"weight": 258,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-recovery-codes.md",
@@ -1582,7 +1586,7 @@
"x-appwrite": {
"method": "updateMfaRecoveryCodes",
"group": "mfa",
"weight": 294,
"weight": 259,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-recovery-codes.md",
@@ -1796,7 +1800,8 @@
"oldPassword": {
"type": "string",
"description": "Current user password. Must be at least 8 chars.",
"x-example": "password"
"x-example": "password",
"format": "password"
}
},
"required": [
@@ -1868,12 +1873,14 @@
"phone": {
"type": "string",
"description": "Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212.",
"x-example": "+12065550100"
"x-example": "+12065550100",
"format": "phone"
},
"password": {
"type": "string",
"description": "User password. Must be at least 8 chars.",
"x-example": "password"
"x-example": "password",
"format": "password"
}
},
"required": [
@@ -2072,12 +2079,14 @@
"email": {
"type": "string",
"description": "User email.",
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"url": {
"type": "string",
"description": "URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.",
"x-example": "https:\/\/example.com"
"x-example": "https:\/\/example.com",
"format": "url"
}
},
"required": [
@@ -2382,12 +2391,14 @@
"email": {
"type": "string",
"description": "User email.",
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"password": {
"type": "string",
"description": "User password. Must be at least 8 chars.",
"x-example": "password"
"x-example": "password",
"format": "password"
}
},
"required": [
@@ -2573,9 +2584,7 @@
"yammer",
"yandex",
"zoho",
"zoom",
"mock",
"mock-unverified"
"zoom"
],
"x-enum-name": "OAuthProvider",
"x-enum-keys": []
@@ -3304,7 +3313,8 @@
"email": {
"type": "string",
"description": "User email.",
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"phrase": {
"type": "boolean",
@@ -3390,12 +3400,14 @@
"email": {
"type": "string",
"description": "User email.",
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"url": {
"type": "string",
"description": "URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.",
"x-example": "https:\/\/example.com"
"x-example": "https:\/\/example.com",
"format": "url"
},
"phrase": {
"type": "boolean",
@@ -3504,9 +3516,7 @@
"yammer",
"yandex",
"zoho",
"zoom",
"mock",
"mock-unverified"
"zoom"
],
"x-enum-name": "OAuthProvider",
"x-enum-keys": []
@@ -3621,7 +3631,8 @@
"phone": {
"type": "string",
"description": "Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212.",
"x-example": "+12065550100"
"x-example": "+12065550100",
"format": "phone"
}
},
"required": [
@@ -3746,7 +3757,8 @@
"url": {
"type": "string",
"description": "URL to redirect the user back to your app from the verification email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.",
"x-example": "https:\/\/example.com"
"x-example": "https:\/\/example.com",
"format": "url"
}
},
"required": [
@@ -4039,7 +4051,7 @@
"x-appwrite": {
"method": "getBrowser",
"group": null,
"weight": 50,
"weight": 264,
"cookies": false,
"type": "location",
"demo": "avatars\/get-browser.md",
@@ -4167,7 +4179,7 @@
"x-appwrite": {
"method": "getCreditCard",
"group": null,
"weight": 49,
"weight": 263,
"cookies": false,
"type": "location",
"demo": "avatars\/get-credit-card.md",
@@ -4301,7 +4313,7 @@
"x-appwrite": {
"method": "getFavicon",
"group": null,
"weight": 53,
"weight": 267,
"cookies": false,
"type": "location",
"demo": "avatars\/get-favicon.md",
@@ -4361,7 +4373,7 @@
"x-appwrite": {
"method": "getFlag",
"group": null,
"weight": 51,
"weight": 265,
"cookies": false,
"type": "location",
"demo": "avatars\/get-flag.md",
@@ -4851,7 +4863,7 @@
"x-appwrite": {
"method": "getImage",
"group": null,
"weight": 52,
"weight": 266,
"cookies": false,
"type": "location",
"demo": "avatars\/get-image.md",
@@ -4935,7 +4947,7 @@
"x-appwrite": {
"method": "getInitials",
"group": null,
"weight": 55,
"weight": 269,
"cookies": false,
"type": "location",
"demo": "avatars\/get-initials.md",
@@ -5029,7 +5041,7 @@
"x-appwrite": {
"method": "getQR",
"group": null,
"weight": 54,
"weight": 268,
"cookies": false,
"type": "location",
"demo": "avatars\/get-qr.md",
@@ -5123,7 +5135,7 @@
"x-appwrite": {
"method": "getScreenshot",
"group": null,
"weight": 56,
"weight": 270,
"cookies": false,
"type": "location",
"demo": "avatars\/get-screenshot.md",
@@ -5876,7 +5888,7 @@
"x-appwrite": {
"method": "listTransactions",
"group": "transactions",
"weight": 362,
"weight": 338,
"cookies": false,
"type": "",
"demo": "databases\/list-transactions.md",
@@ -5943,7 +5955,7 @@
"x-appwrite": {
"method": "createTransaction",
"group": "transactions",
"weight": 358,
"weight": 334,
"cookies": false,
"type": "",
"demo": "databases\/create-transaction.md",
@@ -5980,7 +5992,8 @@
"ttl": {
"type": "integer",
"description": "Seconds before the transaction expires.",
"x-example": 60
"x-example": 60,
"format": "int32"
}
}
}
@@ -6013,7 +6026,7 @@
"x-appwrite": {
"method": "getTransaction",
"group": "transactions",
"weight": 359,
"weight": 335,
"cookies": false,
"type": "",
"demo": "databases\/get-transaction.md",
@@ -6077,7 +6090,7 @@
"x-appwrite": {
"method": "updateTransaction",
"group": "transactions",
"weight": 360,
"weight": 336,
"cookies": false,
"type": "",
"demo": "databases\/update-transaction.md",
@@ -6155,7 +6168,7 @@
"x-appwrite": {
"method": "deleteTransaction",
"group": "transactions",
"weight": 361,
"weight": 337,
"cookies": false,
"type": "",
"demo": "databases\/delete-transaction.md",
@@ -6221,7 +6234,7 @@
"x-appwrite": {
"method": "createOperations",
"group": "transactions",
"weight": 363,
"weight": 339,
"cookies": false,
"type": "",
"demo": "databases\/create-operations.md",
@@ -6306,7 +6319,7 @@
"x-appwrite": {
"method": "listDocuments",
"group": "documents",
"weight": 321,
"weight": 297,
"cookies": false,
"type": "",
"demo": "databases\/list-documents.md",
@@ -6418,7 +6431,7 @@
"x-appwrite": {
"method": "createDocument",
"group": "documents",
"weight": 313,
"weight": 289,
"cookies": false,
"type": "",
"demo": "databases\/create-document.md",
@@ -6579,7 +6592,7 @@
"x-appwrite": {
"method": "getDocument",
"group": "documents",
"weight": 314,
"weight": 290,
"cookies": false,
"type": "",
"demo": "databases\/get-document.md",
@@ -6690,7 +6703,7 @@
"x-appwrite": {
"method": "upsertDocument",
"group": "documents",
"weight": 317,
"weight": 293,
"cookies": false,
"type": "",
"demo": "databases\/upsert-document.md",
@@ -6845,7 +6858,7 @@
"x-appwrite": {
"method": "updateDocument",
"group": "documents",
"weight": 315,
"weight": 291,
"cookies": false,
"type": "",
"demo": "databases\/update-document.md",
@@ -6957,7 +6970,7 @@
"x-appwrite": {
"method": "deleteDocument",
"group": "documents",
"weight": 319,
"weight": 295,
"cookies": false,
"type": "",
"demo": "databases\/delete-document.md",
@@ -7064,7 +7077,7 @@
"x-appwrite": {
"method": "decrementDocumentAttribute",
"group": "documents",
"weight": 324,
"weight": 300,
"cookies": false,
"type": "",
"demo": "databases\/decrement-document-attribute.md",
@@ -7146,12 +7159,14 @@
"value": {
"type": "number",
"description": "Value to increment the attribute by. The value must be a number.",
"x-example": null
"x-example": null,
"format": "float"
},
"min": {
"type": "number",
"description": "Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.",
"x-example": null,
"format": "float",
"x-nullable": true
},
"transactionId": {
@@ -7191,7 +7206,7 @@
"x-appwrite": {
"method": "incrementDocumentAttribute",
"group": "documents",
"weight": 323,
"weight": 299,
"cookies": false,
"type": "",
"demo": "databases\/increment-document-attribute.md",
@@ -7273,12 +7288,14 @@
"value": {
"type": "number",
"description": "Value to increment the attribute by. The value must be a number.",
"x-example": null
"x-example": null,
"format": "float"
},
"max": {
"type": "number",
"description": "Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.",
"x-example": null,
"format": "float",
"x-nullable": true
},
"transactionId": {
@@ -7318,7 +7335,7 @@
"x-appwrite": {
"method": "listExecutions",
"group": "executions",
"weight": 454,
"weight": 431,
"cookies": false,
"type": "",
"demo": "functions\/list-executions.md",
@@ -7405,7 +7422,7 @@
"x-appwrite": {
"method": "createExecution",
"group": "executions",
"weight": 452,
"weight": 429,
"cookies": false,
"type": "",
"demo": "functions\/create-execution.md",
@@ -7523,7 +7540,7 @@
"x-appwrite": {
"method": "getExecution",
"group": "executions",
"weight": 453,
"weight": 430,
"cookies": false,
"type": "",
"demo": "functions\/get-execution.md",
@@ -7598,7 +7615,7 @@
"x-appwrite": {
"method": "query",
"group": "graphql",
"weight": 225,
"weight": 190,
"cookies": false,
"type": "graphql",
"demo": "graphql\/query.md",
@@ -7652,7 +7669,7 @@
"x-appwrite": {
"method": "mutation",
"group": "graphql",
"weight": 224,
"weight": 189,
"cookies": false,
"type": "graphql",
"demo": "graphql\/mutation.md",
@@ -7706,7 +7723,7 @@
"x-appwrite": {
"method": "get",
"group": null,
"weight": 60,
"weight": 49,
"cookies": false,
"type": "",
"demo": "locale\/get.md",
@@ -7760,7 +7777,7 @@
"x-appwrite": {
"method": "listCodes",
"group": null,
"weight": 61,
"weight": 50,
"cookies": false,
"type": "",
"demo": "locale\/list-codes.md",
@@ -7814,7 +7831,7 @@
"x-appwrite": {
"method": "listContinents",
"group": null,
"weight": 65,
"weight": 54,
"cookies": false,
"type": "",
"demo": "locale\/list-continents.md",
@@ -7868,7 +7885,7 @@
"x-appwrite": {
"method": "listCountries",
"group": null,
"weight": 62,
"weight": 51,
"cookies": false,
"type": "",
"demo": "locale\/list-countries.md",
@@ -7922,7 +7939,7 @@
"x-appwrite": {
"method": "listCountriesEU",
"group": null,
"weight": 63,
"weight": 52,
"cookies": false,
"type": "",
"demo": "locale\/list-countries-eu.md",
@@ -7976,7 +7993,7 @@
"x-appwrite": {
"method": "listCountriesPhones",
"group": null,
"weight": 64,
"weight": 53,
"cookies": false,
"type": "",
"demo": "locale\/list-countries-phones.md",
@@ -8030,7 +8047,7 @@
"x-appwrite": {
"method": "listCurrencies",
"group": null,
"weight": 66,
"weight": 55,
"cookies": false,
"type": "",
"demo": "locale\/list-currencies.md",
@@ -8084,7 +8101,7 @@
"x-appwrite": {
"method": "listLanguages",
"group": null,
"weight": 67,
"weight": 56,
"cookies": false,
"type": "",
"demo": "locale\/list-languages.md",
@@ -8138,7 +8155,7 @@
"x-appwrite": {
"method": "createSubscriber",
"group": "subscribers",
"weight": 272,
"weight": 237,
"cookies": false,
"type": "",
"demo": "messaging\/create-subscriber.md",
@@ -8222,7 +8239,7 @@
"x-appwrite": {
"method": "deleteSubscriber",
"group": "subscribers",
"weight": 276,
"weight": 241,
"cookies": false,
"type": "",
"demo": "messaging\/delete-subscriber.md",
@@ -8298,7 +8315,7 @@
"x-appwrite": {
"method": "listFiles",
"group": "files",
"weight": 525,
"weight": 527,
"cookies": false,
"type": "",
"demo": "storage\/list-files.md",
@@ -8397,7 +8414,7 @@
"x-appwrite": {
"method": "createFile",
"group": "files",
"weight": 523,
"weight": 525,
"cookies": false,
"type": "upload",
"demo": "storage\/create-file.md",
@@ -8452,7 +8469,8 @@
"file": {
"type": "string",
"description": "Binary file. Appwrite SDKs provide helpers to handle file input. [Learn about file input](https:\/\/appwrite.io\/docs\/products\/storage\/upload-download#input-file).",
"x-example": null
"x-example": null,
"format": "binary"
},
"permissions": {
"type": "array",
@@ -8498,7 +8516,7 @@
"x-appwrite": {
"method": "getFile",
"group": "files",
"weight": 524,
"weight": 526,
"cookies": false,
"type": "",
"demo": "storage\/get-file.md",
@@ -8572,7 +8590,7 @@
"x-appwrite": {
"method": "updateFile",
"group": "files",
"weight": 526,
"weight": 528,
"cookies": false,
"type": "",
"demo": "storage\/update-file.md",
@@ -8664,7 +8682,7 @@
"x-appwrite": {
"method": "deleteFile",
"group": "files",
"weight": 527,
"weight": 529,
"cookies": false,
"type": "",
"demo": "storage\/delete-file.md",
@@ -8733,7 +8751,7 @@
"x-appwrite": {
"method": "getFileDownload",
"group": "files",
"weight": 529,
"weight": 531,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-download.md",
@@ -8813,7 +8831,7 @@
"x-appwrite": {
"method": "getFilePreview",
"group": "files",
"weight": 528,
"weight": 530,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-preview.md",
@@ -9043,7 +9061,7 @@
"x-appwrite": {
"method": "getFileView",
"group": "files",
"weight": 530,
"weight": 532,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-view.md",
@@ -9130,7 +9148,7 @@
"x-appwrite": {
"method": "listTransactions",
"group": "transactions",
"weight": 427,
"weight": 403,
"cookies": false,
"type": "",
"demo": "tablesdb\/list-transactions.md",
@@ -9200,7 +9218,7 @@
"x-appwrite": {
"method": "createTransaction",
"group": "transactions",
"weight": 423,
"weight": 399,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-transaction.md",
@@ -9240,7 +9258,8 @@
"ttl": {
"type": "integer",
"description": "Seconds before the transaction expires.",
"x-example": 60
"x-example": 60,
"format": "int32"
}
}
}
@@ -9273,7 +9292,7 @@
"x-appwrite": {
"method": "getTransaction",
"group": "transactions",
"weight": 424,
"weight": 400,
"cookies": false,
"type": "",
"demo": "tablesdb\/get-transaction.md",
@@ -9340,7 +9359,7 @@
"x-appwrite": {
"method": "updateTransaction",
"group": "transactions",
"weight": 425,
"weight": 401,
"cookies": false,
"type": "",
"demo": "tablesdb\/update-transaction.md",
@@ -9421,7 +9440,7 @@
"x-appwrite": {
"method": "deleteTransaction",
"group": "transactions",
"weight": 426,
"weight": 402,
"cookies": false,
"type": "",
"demo": "tablesdb\/delete-transaction.md",
@@ -9490,7 +9509,7 @@
"x-appwrite": {
"method": "createOperations",
"group": "transactions",
"weight": 428,
"weight": 404,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-operations.md",
@@ -9578,7 +9597,7 @@
"x-appwrite": {
"method": "listRows",
"group": "rows",
"weight": 419,
"weight": 395,
"cookies": false,
"type": "",
"demo": "tablesdb\/list-rows.md",
@@ -9689,7 +9708,7 @@
"x-appwrite": {
"method": "createRow",
"group": "rows",
"weight": 411,
"weight": 387,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-row.md",
@@ -9845,7 +9864,7 @@
"x-appwrite": {
"method": "getRow",
"group": "rows",
"weight": 412,
"weight": 388,
"cookies": false,
"type": "",
"demo": "tablesdb\/get-row.md",
@@ -9955,7 +9974,7 @@
"x-appwrite": {
"method": "upsertRow",
"group": "rows",
"weight": 415,
"weight": 391,
"cookies": false,
"type": "",
"demo": "tablesdb\/upsert-row.md",
@@ -10105,7 +10124,7 @@
"x-appwrite": {
"method": "updateRow",
"group": "rows",
"weight": 413,
"weight": 389,
"cookies": false,
"type": "",
"demo": "tablesdb\/update-row.md",
@@ -10216,7 +10235,7 @@
"x-appwrite": {
"method": "deleteRow",
"group": "rows",
"weight": 417,
"weight": 393,
"cookies": false,
"type": "",
"demo": "tablesdb\/delete-row.md",
@@ -10322,7 +10341,7 @@
"x-appwrite": {
"method": "decrementRowColumn",
"group": "rows",
"weight": 422,
"weight": 398,
"cookies": false,
"type": "",
"demo": "tablesdb\/decrement-row-column.md",
@@ -10403,12 +10422,14 @@
"value": {
"type": "number",
"description": "Value to increment the column by. The value must be a number.",
"x-example": null
"x-example": null,
"format": "float"
},
"min": {
"type": "number",
"description": "Minimum value for the column. If the current value is lesser than this value, an exception will be thrown.",
"x-example": null,
"format": "float",
"x-nullable": true
},
"transactionId": {
@@ -10448,7 +10469,7 @@
"x-appwrite": {
"method": "incrementRowColumn",
"group": "rows",
"weight": 421,
"weight": 397,
"cookies": false,
"type": "",
"demo": "tablesdb\/increment-row-column.md",
@@ -10529,12 +10550,14 @@
"value": {
"type": "number",
"description": "Value to increment the column by. The value must be a number.",
"x-example": null
"x-example": null,
"format": "float"
},
"max": {
"type": "number",
"description": "Maximum value for the column. If the current value is greater than this value, an error will be thrown.",
"x-example": null,
"format": "float",
"x-nullable": true
},
"transactionId": {
@@ -10574,7 +10597,7 @@
"x-appwrite": {
"method": "list",
"group": "teams",
"weight": 145,
"weight": 110,
"cookies": false,
"type": "",
"demo": "teams\/list.md",
@@ -10663,7 +10686,7 @@
"x-appwrite": {
"method": "create",
"group": "teams",
"weight": 144,
"weight": 109,
"cookies": false,
"type": "",
"demo": "teams\/create.md",
@@ -10750,7 +10773,7 @@
"x-appwrite": {
"method": "get",
"group": "teams",
"weight": 146,
"weight": 111,
"cookies": false,
"type": "",
"demo": "teams\/get.md",
@@ -10814,7 +10837,7 @@
"x-appwrite": {
"method": "updateName",
"group": "teams",
"weight": 148,
"weight": 113,
"cookies": false,
"type": "",
"demo": "teams\/update-name.md",
@@ -10890,7 +10913,7 @@
"x-appwrite": {
"method": "delete",
"group": "teams",
"weight": 150,
"weight": 115,
"cookies": false,
"type": "",
"demo": "teams\/delete.md",
@@ -10956,7 +10979,7 @@
"x-appwrite": {
"method": "listMemberships",
"group": "memberships",
"weight": 152,
"weight": 117,
"cookies": false,
"type": "",
"demo": "teams\/list-memberships.md",
@@ -11055,7 +11078,7 @@
"x-appwrite": {
"method": "createMembership",
"group": "memberships",
"weight": 151,
"weight": 116,
"cookies": false,
"type": "",
"demo": "teams\/create-membership.md",
@@ -11104,7 +11127,8 @@
"email": {
"type": "string",
"description": "Email of the new team member.",
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"userId": {
"type": "string",
@@ -11114,7 +11138,8 @@
"phone": {
"type": "string",
"description": "Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212.",
"x-example": "+12065550100"
"x-example": "+12065550100",
"format": "phone"
},
"roles": {
"type": "array",
@@ -11134,7 +11159,8 @@
"url": {
"type": "string",
"description": "URL to redirect the user back to your app from the invitation email. This parameter is not required when an API key is supplied. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.",
"x-example": "https:\/\/example.com"
"x-example": "https:\/\/example.com",
"format": "url"
},
"name": {
"type": "string",
@@ -11175,7 +11201,7 @@
"x-appwrite": {
"method": "getMembership",
"group": "memberships",
"weight": 153,
"weight": 118,
"cookies": false,
"type": "",
"demo": "teams\/get-membership.md",
@@ -11249,7 +11275,7 @@
"x-appwrite": {
"method": "updateMembership",
"group": "memberships",
"weight": 154,
"weight": 119,
"cookies": false,
"type": "",
"demo": "teams\/update-membership.md",
@@ -11345,7 +11371,7 @@
"x-appwrite": {
"method": "deleteMembership",
"group": "memberships",
"weight": 156,
"weight": 121,
"cookies": false,
"type": "",
"demo": "teams\/delete-membership.md",
@@ -11421,7 +11447,7 @@
"x-appwrite": {
"method": "updateMembershipStatus",
"group": "memberships",
"weight": 155,
"weight": 120,
"cookies": false,
"type": "",
"demo": "teams\/update-membership-status.md",
@@ -11521,7 +11547,7 @@
"x-appwrite": {
"method": "getPrefs",
"group": "teams",
"weight": 147,
"weight": 112,
"cookies": false,
"type": "",
"demo": "teams\/get-prefs.md",
@@ -11584,7 +11610,7 @@
"x-appwrite": {
"method": "updatePrefs",
"group": "teams",
"weight": 149,
"weight": 114,
"cookies": false,
"type": "",
"demo": "teams\/update-prefs.md",
@@ -13428,6 +13454,16 @@
"description": "Total number of chunks uploaded",
"x-example": 17890,
"format": "int32"
},
"encryption": {
"type": "boolean",
"description": "Whether file contents are encrypted at rest.",
"x-example": true
},
"compression": {
"type": "string",
"description": "Compression algorithm used for the file. Will be one of none, [gzip](https:\/\/en.wikipedia.org\/wiki\/Gzip), or [zstd](https:\/\/en.wikipedia.org\/wiki\/Zstd).",
"x-example": "gzip"
}
},
"required": [
@@ -13441,7 +13477,9 @@
"mimeType",
"sizeOriginal",
"chunksTotal",
"chunksUploaded"
"chunksUploaded",
"encryption",
"compression"
],
"example": {
"$id": "5e5ea5c16897e",
@@ -13456,7 +13494,9 @@
"mimeType": "image\/png",
"sizeOriginal": 17890,
"chunksTotal": 17890,
"chunksUploaded": 17890
"chunksUploaded": 17890,
"encryption": true,
"compression": "gzip"
}
},
"team": {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+156 -116
View File
@@ -140,7 +140,8 @@
"email": {
"type": "string",
"description": "User email.",
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"password": {
"type": "string",
@@ -224,12 +225,14 @@
"email": {
"type": "string",
"description": "User email.",
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"password": {
"type": "string",
"description": "User password. Must be at least 8 chars.",
"x-example": "password"
"x-example": "password",
"format": "password"
}
},
"required": [
@@ -439,7 +442,8 @@
"duration": {
"type": "integer",
"description": "Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds.",
"x-example": 0
"x-example": 0,
"format": "int32"
}
}
}
@@ -551,7 +555,7 @@
"x-appwrite": {
"method": "updateMFA",
"group": "mfa",
"weight": 288,
"weight": 253,
"cookies": false,
"type": "",
"demo": "account\/update-mfa.md",
@@ -623,7 +627,7 @@
"x-appwrite": {
"method": "createMfaAuthenticator",
"group": "mfa",
"weight": 290,
"weight": 255,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-authenticator.md",
@@ -747,7 +751,7 @@
"x-appwrite": {
"method": "updateMfaAuthenticator",
"group": "mfa",
"weight": 291,
"weight": 256,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-authenticator.md",
@@ -887,7 +891,7 @@
"x-appwrite": {
"method": "deleteMfaAuthenticator",
"group": "mfa",
"weight": 292,
"weight": 257,
"cookies": false,
"type": "",
"demo": "account\/delete-mfa-authenticator.md",
@@ -1011,7 +1015,7 @@
"x-appwrite": {
"method": "createMfaChallenge",
"group": "mfa",
"weight": 296,
"weight": 261,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-challenge.md",
@@ -1145,7 +1149,7 @@
"x-appwrite": {
"method": "updateMfaChallenge",
"group": "mfa",
"weight": 297,
"weight": 262,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-challenge.md",
@@ -1283,7 +1287,7 @@
"x-appwrite": {
"method": "listMfaFactors",
"group": "mfa",
"weight": 289,
"weight": 254,
"cookies": false,
"type": "",
"demo": "account\/list-mfa-factors.md",
@@ -1384,7 +1388,7 @@
"x-appwrite": {
"method": "getMfaRecoveryCodes",
"group": "mfa",
"weight": 295,
"weight": 260,
"cookies": false,
"type": "",
"demo": "account\/get-mfa-recovery-codes.md",
@@ -1483,7 +1487,7 @@
"x-appwrite": {
"method": "createMfaRecoveryCodes",
"group": "mfa",
"weight": 293,
"weight": 258,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-recovery-codes.md",
@@ -1582,7 +1586,7 @@
"x-appwrite": {
"method": "updateMfaRecoveryCodes",
"group": "mfa",
"weight": 294,
"weight": 259,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-recovery-codes.md",
@@ -1796,7 +1800,8 @@
"oldPassword": {
"type": "string",
"description": "Current user password. Must be at least 8 chars.",
"x-example": "password"
"x-example": "password",
"format": "password"
}
},
"required": [
@@ -1868,12 +1873,14 @@
"phone": {
"type": "string",
"description": "Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212.",
"x-example": "+12065550100"
"x-example": "+12065550100",
"format": "phone"
},
"password": {
"type": "string",
"description": "User password. Must be at least 8 chars.",
"x-example": "password"
"x-example": "password",
"format": "password"
}
},
"required": [
@@ -2072,12 +2079,14 @@
"email": {
"type": "string",
"description": "User email.",
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"url": {
"type": "string",
"description": "URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.",
"x-example": "https:\/\/example.com"
"x-example": "https:\/\/example.com",
"format": "url"
}
},
"required": [
@@ -2382,12 +2391,14 @@
"email": {
"type": "string",
"description": "User email.",
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"password": {
"type": "string",
"description": "User password. Must be at least 8 chars.",
"x-example": "password"
"x-example": "password",
"format": "password"
}
},
"required": [
@@ -2573,9 +2584,7 @@
"yammer",
"yandex",
"zoho",
"zoom",
"mock",
"mock-unverified"
"zoom"
],
"x-enum-name": "OAuthProvider",
"x-enum-keys": []
@@ -3304,7 +3313,8 @@
"email": {
"type": "string",
"description": "User email.",
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"phrase": {
"type": "boolean",
@@ -3390,12 +3400,14 @@
"email": {
"type": "string",
"description": "User email.",
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"url": {
"type": "string",
"description": "URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.",
"x-example": "https:\/\/example.com"
"x-example": "https:\/\/example.com",
"format": "url"
},
"phrase": {
"type": "boolean",
@@ -3504,9 +3516,7 @@
"yammer",
"yandex",
"zoho",
"zoom",
"mock",
"mock-unverified"
"zoom"
],
"x-enum-name": "OAuthProvider",
"x-enum-keys": []
@@ -3621,7 +3631,8 @@
"phone": {
"type": "string",
"description": "Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212.",
"x-example": "+12065550100"
"x-example": "+12065550100",
"format": "phone"
}
},
"required": [
@@ -3746,7 +3757,8 @@
"url": {
"type": "string",
"description": "URL to redirect the user back to your app from the verification email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.",
"x-example": "https:\/\/example.com"
"x-example": "https:\/\/example.com",
"format": "url"
}
},
"required": [
@@ -4039,7 +4051,7 @@
"x-appwrite": {
"method": "getBrowser",
"group": null,
"weight": 50,
"weight": 264,
"cookies": false,
"type": "location",
"demo": "avatars\/get-browser.md",
@@ -4167,7 +4179,7 @@
"x-appwrite": {
"method": "getCreditCard",
"group": null,
"weight": 49,
"weight": 263,
"cookies": false,
"type": "location",
"demo": "avatars\/get-credit-card.md",
@@ -4301,7 +4313,7 @@
"x-appwrite": {
"method": "getFavicon",
"group": null,
"weight": 53,
"weight": 267,
"cookies": false,
"type": "location",
"demo": "avatars\/get-favicon.md",
@@ -4361,7 +4373,7 @@
"x-appwrite": {
"method": "getFlag",
"group": null,
"weight": 51,
"weight": 265,
"cookies": false,
"type": "location",
"demo": "avatars\/get-flag.md",
@@ -4851,7 +4863,7 @@
"x-appwrite": {
"method": "getImage",
"group": null,
"weight": 52,
"weight": 266,
"cookies": false,
"type": "location",
"demo": "avatars\/get-image.md",
@@ -4935,7 +4947,7 @@
"x-appwrite": {
"method": "getInitials",
"group": null,
"weight": 55,
"weight": 269,
"cookies": false,
"type": "location",
"demo": "avatars\/get-initials.md",
@@ -5029,7 +5041,7 @@
"x-appwrite": {
"method": "getQR",
"group": null,
"weight": 54,
"weight": 268,
"cookies": false,
"type": "location",
"demo": "avatars\/get-qr.md",
@@ -5123,7 +5135,7 @@
"x-appwrite": {
"method": "getScreenshot",
"group": null,
"weight": 56,
"weight": 270,
"cookies": false,
"type": "location",
"demo": "avatars\/get-screenshot.md",
@@ -5876,7 +5888,7 @@
"x-appwrite": {
"method": "listTransactions",
"group": "transactions",
"weight": 362,
"weight": 338,
"cookies": false,
"type": "",
"demo": "databases\/list-transactions.md",
@@ -5943,7 +5955,7 @@
"x-appwrite": {
"method": "createTransaction",
"group": "transactions",
"weight": 358,
"weight": 334,
"cookies": false,
"type": "",
"demo": "databases\/create-transaction.md",
@@ -5980,7 +5992,8 @@
"ttl": {
"type": "integer",
"description": "Seconds before the transaction expires.",
"x-example": 60
"x-example": 60,
"format": "int32"
}
}
}
@@ -6013,7 +6026,7 @@
"x-appwrite": {
"method": "getTransaction",
"group": "transactions",
"weight": 359,
"weight": 335,
"cookies": false,
"type": "",
"demo": "databases\/get-transaction.md",
@@ -6077,7 +6090,7 @@
"x-appwrite": {
"method": "updateTransaction",
"group": "transactions",
"weight": 360,
"weight": 336,
"cookies": false,
"type": "",
"demo": "databases\/update-transaction.md",
@@ -6155,7 +6168,7 @@
"x-appwrite": {
"method": "deleteTransaction",
"group": "transactions",
"weight": 361,
"weight": 337,
"cookies": false,
"type": "",
"demo": "databases\/delete-transaction.md",
@@ -6221,7 +6234,7 @@
"x-appwrite": {
"method": "createOperations",
"group": "transactions",
"weight": 363,
"weight": 339,
"cookies": false,
"type": "",
"demo": "databases\/create-operations.md",
@@ -6306,7 +6319,7 @@
"x-appwrite": {
"method": "listDocuments",
"group": "documents",
"weight": 321,
"weight": 297,
"cookies": false,
"type": "",
"demo": "databases\/list-documents.md",
@@ -6418,7 +6431,7 @@
"x-appwrite": {
"method": "createDocument",
"group": "documents",
"weight": 313,
"weight": 289,
"cookies": false,
"type": "",
"demo": "databases\/create-document.md",
@@ -6579,7 +6592,7 @@
"x-appwrite": {
"method": "getDocument",
"group": "documents",
"weight": 314,
"weight": 290,
"cookies": false,
"type": "",
"demo": "databases\/get-document.md",
@@ -6690,7 +6703,7 @@
"x-appwrite": {
"method": "upsertDocument",
"group": "documents",
"weight": 317,
"weight": 293,
"cookies": false,
"type": "",
"demo": "databases\/upsert-document.md",
@@ -6845,7 +6858,7 @@
"x-appwrite": {
"method": "updateDocument",
"group": "documents",
"weight": 315,
"weight": 291,
"cookies": false,
"type": "",
"demo": "databases\/update-document.md",
@@ -6957,7 +6970,7 @@
"x-appwrite": {
"method": "deleteDocument",
"group": "documents",
"weight": 319,
"weight": 295,
"cookies": false,
"type": "",
"demo": "databases\/delete-document.md",
@@ -7064,7 +7077,7 @@
"x-appwrite": {
"method": "decrementDocumentAttribute",
"group": "documents",
"weight": 324,
"weight": 300,
"cookies": false,
"type": "",
"demo": "databases\/decrement-document-attribute.md",
@@ -7146,12 +7159,14 @@
"value": {
"type": "number",
"description": "Value to increment the attribute by. The value must be a number.",
"x-example": null
"x-example": null,
"format": "float"
},
"min": {
"type": "number",
"description": "Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.",
"x-example": null,
"format": "float",
"x-nullable": true
},
"transactionId": {
@@ -7191,7 +7206,7 @@
"x-appwrite": {
"method": "incrementDocumentAttribute",
"group": "documents",
"weight": 323,
"weight": 299,
"cookies": false,
"type": "",
"demo": "databases\/increment-document-attribute.md",
@@ -7273,12 +7288,14 @@
"value": {
"type": "number",
"description": "Value to increment the attribute by. The value must be a number.",
"x-example": null
"x-example": null,
"format": "float"
},
"max": {
"type": "number",
"description": "Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.",
"x-example": null,
"format": "float",
"x-nullable": true
},
"transactionId": {
@@ -7318,7 +7335,7 @@
"x-appwrite": {
"method": "listExecutions",
"group": "executions",
"weight": 455,
"weight": 431,
"cookies": false,
"type": "",
"demo": "functions\/list-executions.md",
@@ -7405,7 +7422,7 @@
"x-appwrite": {
"method": "createExecution",
"group": "executions",
"weight": 453,
"weight": 429,
"cookies": false,
"type": "",
"demo": "functions\/create-execution.md",
@@ -7523,7 +7540,7 @@
"x-appwrite": {
"method": "getExecution",
"group": "executions",
"weight": 454,
"weight": 430,
"cookies": false,
"type": "",
"demo": "functions\/get-execution.md",
@@ -7598,7 +7615,7 @@
"x-appwrite": {
"method": "query",
"group": "graphql",
"weight": 225,
"weight": 190,
"cookies": false,
"type": "graphql",
"demo": "graphql\/query.md",
@@ -7652,7 +7669,7 @@
"x-appwrite": {
"method": "mutation",
"group": "graphql",
"weight": 224,
"weight": 189,
"cookies": false,
"type": "graphql",
"demo": "graphql\/mutation.md",
@@ -7706,7 +7723,7 @@
"x-appwrite": {
"method": "get",
"group": null,
"weight": 60,
"weight": 49,
"cookies": false,
"type": "",
"demo": "locale\/get.md",
@@ -7760,7 +7777,7 @@
"x-appwrite": {
"method": "listCodes",
"group": null,
"weight": 61,
"weight": 50,
"cookies": false,
"type": "",
"demo": "locale\/list-codes.md",
@@ -7814,7 +7831,7 @@
"x-appwrite": {
"method": "listContinents",
"group": null,
"weight": 65,
"weight": 54,
"cookies": false,
"type": "",
"demo": "locale\/list-continents.md",
@@ -7868,7 +7885,7 @@
"x-appwrite": {
"method": "listCountries",
"group": null,
"weight": 62,
"weight": 51,
"cookies": false,
"type": "",
"demo": "locale\/list-countries.md",
@@ -7922,7 +7939,7 @@
"x-appwrite": {
"method": "listCountriesEU",
"group": null,
"weight": 63,
"weight": 52,
"cookies": false,
"type": "",
"demo": "locale\/list-countries-eu.md",
@@ -7976,7 +7993,7 @@
"x-appwrite": {
"method": "listCountriesPhones",
"group": null,
"weight": 64,
"weight": 53,
"cookies": false,
"type": "",
"demo": "locale\/list-countries-phones.md",
@@ -8030,7 +8047,7 @@
"x-appwrite": {
"method": "listCurrencies",
"group": null,
"weight": 66,
"weight": 55,
"cookies": false,
"type": "",
"demo": "locale\/list-currencies.md",
@@ -8084,7 +8101,7 @@
"x-appwrite": {
"method": "listLanguages",
"group": null,
"weight": 67,
"weight": 56,
"cookies": false,
"type": "",
"demo": "locale\/list-languages.md",
@@ -8138,7 +8155,7 @@
"x-appwrite": {
"method": "createSubscriber",
"group": "subscribers",
"weight": 272,
"weight": 237,
"cookies": false,
"type": "",
"demo": "messaging\/create-subscriber.md",
@@ -8222,7 +8239,7 @@
"x-appwrite": {
"method": "deleteSubscriber",
"group": "subscribers",
"weight": 276,
"weight": 241,
"cookies": false,
"type": "",
"demo": "messaging\/delete-subscriber.md",
@@ -8298,7 +8315,7 @@
"x-appwrite": {
"method": "listFiles",
"group": "files",
"weight": 526,
"weight": 527,
"cookies": false,
"type": "",
"demo": "storage\/list-files.md",
@@ -8397,7 +8414,7 @@
"x-appwrite": {
"method": "createFile",
"group": "files",
"weight": 524,
"weight": 525,
"cookies": false,
"type": "upload",
"demo": "storage\/create-file.md",
@@ -8452,7 +8469,8 @@
"file": {
"type": "string",
"description": "Binary file. Appwrite SDKs provide helpers to handle file input. [Learn about file input](https:\/\/appwrite.io\/docs\/products\/storage\/upload-download#input-file).",
"x-example": null
"x-example": null,
"format": "binary"
},
"permissions": {
"type": "array",
@@ -8498,7 +8516,7 @@
"x-appwrite": {
"method": "getFile",
"group": "files",
"weight": 525,
"weight": 526,
"cookies": false,
"type": "",
"demo": "storage\/get-file.md",
@@ -8572,7 +8590,7 @@
"x-appwrite": {
"method": "updateFile",
"group": "files",
"weight": 527,
"weight": 528,
"cookies": false,
"type": "",
"demo": "storage\/update-file.md",
@@ -8664,7 +8682,7 @@
"x-appwrite": {
"method": "deleteFile",
"group": "files",
"weight": 528,
"weight": 529,
"cookies": false,
"type": "",
"demo": "storage\/delete-file.md",
@@ -8733,7 +8751,7 @@
"x-appwrite": {
"method": "getFileDownload",
"group": "files",
"weight": 530,
"weight": 531,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-download.md",
@@ -8813,7 +8831,7 @@
"x-appwrite": {
"method": "getFilePreview",
"group": "files",
"weight": 529,
"weight": 530,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-preview.md",
@@ -9043,7 +9061,7 @@
"x-appwrite": {
"method": "getFileView",
"group": "files",
"weight": 531,
"weight": 532,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-view.md",
@@ -9130,7 +9148,7 @@
"x-appwrite": {
"method": "listTransactions",
"group": "transactions",
"weight": 427,
"weight": 403,
"cookies": false,
"type": "",
"demo": "tablesdb\/list-transactions.md",
@@ -9200,7 +9218,7 @@
"x-appwrite": {
"method": "createTransaction",
"group": "transactions",
"weight": 423,
"weight": 399,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-transaction.md",
@@ -9240,7 +9258,8 @@
"ttl": {
"type": "integer",
"description": "Seconds before the transaction expires.",
"x-example": 60
"x-example": 60,
"format": "int32"
}
}
}
@@ -9273,7 +9292,7 @@
"x-appwrite": {
"method": "getTransaction",
"group": "transactions",
"weight": 424,
"weight": 400,
"cookies": false,
"type": "",
"demo": "tablesdb\/get-transaction.md",
@@ -9340,7 +9359,7 @@
"x-appwrite": {
"method": "updateTransaction",
"group": "transactions",
"weight": 425,
"weight": 401,
"cookies": false,
"type": "",
"demo": "tablesdb\/update-transaction.md",
@@ -9421,7 +9440,7 @@
"x-appwrite": {
"method": "deleteTransaction",
"group": "transactions",
"weight": 426,
"weight": 402,
"cookies": false,
"type": "",
"demo": "tablesdb\/delete-transaction.md",
@@ -9490,7 +9509,7 @@
"x-appwrite": {
"method": "createOperations",
"group": "transactions",
"weight": 428,
"weight": 404,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-operations.md",
@@ -9578,7 +9597,7 @@
"x-appwrite": {
"method": "listRows",
"group": "rows",
"weight": 419,
"weight": 395,
"cookies": false,
"type": "",
"demo": "tablesdb\/list-rows.md",
@@ -9689,7 +9708,7 @@
"x-appwrite": {
"method": "createRow",
"group": "rows",
"weight": 411,
"weight": 387,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-row.md",
@@ -9845,7 +9864,7 @@
"x-appwrite": {
"method": "getRow",
"group": "rows",
"weight": 412,
"weight": 388,
"cookies": false,
"type": "",
"demo": "tablesdb\/get-row.md",
@@ -9955,7 +9974,7 @@
"x-appwrite": {
"method": "upsertRow",
"group": "rows",
"weight": 415,
"weight": 391,
"cookies": false,
"type": "",
"demo": "tablesdb\/upsert-row.md",
@@ -10105,7 +10124,7 @@
"x-appwrite": {
"method": "updateRow",
"group": "rows",
"weight": 413,
"weight": 389,
"cookies": false,
"type": "",
"demo": "tablesdb\/update-row.md",
@@ -10216,7 +10235,7 @@
"x-appwrite": {
"method": "deleteRow",
"group": "rows",
"weight": 417,
"weight": 393,
"cookies": false,
"type": "",
"demo": "tablesdb\/delete-row.md",
@@ -10322,7 +10341,7 @@
"x-appwrite": {
"method": "decrementRowColumn",
"group": "rows",
"weight": 422,
"weight": 398,
"cookies": false,
"type": "",
"demo": "tablesdb\/decrement-row-column.md",
@@ -10403,12 +10422,14 @@
"value": {
"type": "number",
"description": "Value to increment the column by. The value must be a number.",
"x-example": null
"x-example": null,
"format": "float"
},
"min": {
"type": "number",
"description": "Minimum value for the column. If the current value is lesser than this value, an exception will be thrown.",
"x-example": null,
"format": "float",
"x-nullable": true
},
"transactionId": {
@@ -10448,7 +10469,7 @@
"x-appwrite": {
"method": "incrementRowColumn",
"group": "rows",
"weight": 421,
"weight": 397,
"cookies": false,
"type": "",
"demo": "tablesdb\/increment-row-column.md",
@@ -10529,12 +10550,14 @@
"value": {
"type": "number",
"description": "Value to increment the column by. The value must be a number.",
"x-example": null
"x-example": null,
"format": "float"
},
"max": {
"type": "number",
"description": "Maximum value for the column. If the current value is greater than this value, an error will be thrown.",
"x-example": null,
"format": "float",
"x-nullable": true
},
"transactionId": {
@@ -10574,7 +10597,7 @@
"x-appwrite": {
"method": "list",
"group": "teams",
"weight": 145,
"weight": 110,
"cookies": false,
"type": "",
"demo": "teams\/list.md",
@@ -10663,7 +10686,7 @@
"x-appwrite": {
"method": "create",
"group": "teams",
"weight": 144,
"weight": 109,
"cookies": false,
"type": "",
"demo": "teams\/create.md",
@@ -10750,7 +10773,7 @@
"x-appwrite": {
"method": "get",
"group": "teams",
"weight": 146,
"weight": 111,
"cookies": false,
"type": "",
"demo": "teams\/get.md",
@@ -10814,7 +10837,7 @@
"x-appwrite": {
"method": "updateName",
"group": "teams",
"weight": 148,
"weight": 113,
"cookies": false,
"type": "",
"demo": "teams\/update-name.md",
@@ -10890,7 +10913,7 @@
"x-appwrite": {
"method": "delete",
"group": "teams",
"weight": 150,
"weight": 115,
"cookies": false,
"type": "",
"demo": "teams\/delete.md",
@@ -10956,7 +10979,7 @@
"x-appwrite": {
"method": "listMemberships",
"group": "memberships",
"weight": 152,
"weight": 117,
"cookies": false,
"type": "",
"demo": "teams\/list-memberships.md",
@@ -11055,7 +11078,7 @@
"x-appwrite": {
"method": "createMembership",
"group": "memberships",
"weight": 151,
"weight": 116,
"cookies": false,
"type": "",
"demo": "teams\/create-membership.md",
@@ -11104,7 +11127,8 @@
"email": {
"type": "string",
"description": "Email of the new team member.",
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"userId": {
"type": "string",
@@ -11114,7 +11138,8 @@
"phone": {
"type": "string",
"description": "Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212.",
"x-example": "+12065550100"
"x-example": "+12065550100",
"format": "phone"
},
"roles": {
"type": "array",
@@ -11134,7 +11159,8 @@
"url": {
"type": "string",
"description": "URL to redirect the user back to your app from the invitation email. This parameter is not required when an API key is supplied. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.",
"x-example": "https:\/\/example.com"
"x-example": "https:\/\/example.com",
"format": "url"
},
"name": {
"type": "string",
@@ -11175,7 +11201,7 @@
"x-appwrite": {
"method": "getMembership",
"group": "memberships",
"weight": 153,
"weight": 118,
"cookies": false,
"type": "",
"demo": "teams\/get-membership.md",
@@ -11249,7 +11275,7 @@
"x-appwrite": {
"method": "updateMembership",
"group": "memberships",
"weight": 154,
"weight": 119,
"cookies": false,
"type": "",
"demo": "teams\/update-membership.md",
@@ -11345,7 +11371,7 @@
"x-appwrite": {
"method": "deleteMembership",
"group": "memberships",
"weight": 156,
"weight": 121,
"cookies": false,
"type": "",
"demo": "teams\/delete-membership.md",
@@ -11421,7 +11447,7 @@
"x-appwrite": {
"method": "updateMembershipStatus",
"group": "memberships",
"weight": 155,
"weight": 120,
"cookies": false,
"type": "",
"demo": "teams\/update-membership-status.md",
@@ -11521,7 +11547,7 @@
"x-appwrite": {
"method": "getPrefs",
"group": "teams",
"weight": 147,
"weight": 112,
"cookies": false,
"type": "",
"demo": "teams\/get-prefs.md",
@@ -11584,7 +11610,7 @@
"x-appwrite": {
"method": "updatePrefs",
"group": "teams",
"weight": 149,
"weight": 114,
"cookies": false,
"type": "",
"demo": "teams\/update-prefs.md",
@@ -13428,6 +13454,16 @@
"description": "Total number of chunks uploaded",
"x-example": 17890,
"format": "int32"
},
"encryption": {
"type": "boolean",
"description": "Whether file contents are encrypted at rest.",
"x-example": true
},
"compression": {
"type": "string",
"description": "Compression algorithm used for the file. Will be one of none, [gzip](https:\/\/en.wikipedia.org\/wiki\/Gzip), or [zstd](https:\/\/en.wikipedia.org\/wiki\/Zstd).",
"x-example": "gzip"
}
},
"required": [
@@ -13441,7 +13477,9 @@
"mimeType",
"sizeOriginal",
"chunksTotal",
"chunksUploaded"
"chunksUploaded",
"encryption",
"compression"
],
"example": {
"$id": "5e5ea5c16897e",
@@ -13456,7 +13494,9 @@
"mimeType": "image\/png",
"sizeOriginal": 17890,
"chunksTotal": 17890,
"chunksUploaded": 17890
"chunksUploaded": 17890,
"encryption": true,
"compression": "gzip"
}
},
"team": {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+155 -116
View File
@@ -1,7 +1,7 @@
{
"swagger": "2.0",
"info": {
"version": "1.8.0",
"version": "1.8.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",
@@ -191,7 +191,8 @@
"type": "string",
"description": "User email.",
"default": null,
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"password": {
"type": "string",
@@ -280,13 +281,15 @@
"type": "string",
"description": "User email.",
"default": null,
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"password": {
"type": "string",
"description": "User password. Must be at least 8 chars.",
"default": null,
"x-example": "password"
"x-example": "password",
"format": "password"
}
},
"required": [
@@ -498,7 +501,8 @@
"type": "integer",
"description": "Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds.",
"default": 900,
"x-example": 0
"x-example": 0,
"format": "int32"
}
}
}
@@ -608,7 +612,7 @@
"x-appwrite": {
"method": "updateMFA",
"group": "mfa",
"weight": 288,
"weight": 253,
"cookies": false,
"type": "",
"demo": "account\/update-mfa.md",
@@ -683,7 +687,7 @@
"x-appwrite": {
"method": "createMfaAuthenticator",
"group": "mfa",
"weight": 290,
"weight": 255,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-authenticator.md",
@@ -807,7 +811,7 @@
"x-appwrite": {
"method": "updateMfaAuthenticator",
"group": "mfa",
"weight": 291,
"weight": 256,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-authenticator.md",
@@ -948,7 +952,7 @@
"x-appwrite": {
"method": "deleteMfaAuthenticator",
"group": "mfa",
"weight": 292,
"weight": 257,
"cookies": false,
"type": "",
"demo": "account\/delete-mfa-authenticator.md",
@@ -1072,7 +1076,7 @@
"x-appwrite": {
"method": "createMfaChallenge",
"group": "mfa",
"weight": 296,
"weight": 261,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-challenge.md",
@@ -1209,7 +1213,7 @@
"x-appwrite": {
"method": "updateMfaChallenge",
"group": "mfa",
"weight": 297,
"weight": 262,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-challenge.md",
@@ -1349,7 +1353,7 @@
"x-appwrite": {
"method": "listMfaFactors",
"group": "mfa",
"weight": 289,
"weight": 254,
"cookies": false,
"type": "",
"demo": "account\/list-mfa-factors.md",
@@ -1450,7 +1454,7 @@
"x-appwrite": {
"method": "getMfaRecoveryCodes",
"group": "mfa",
"weight": 295,
"weight": 260,
"cookies": false,
"type": "",
"demo": "account\/get-mfa-recovery-codes.md",
@@ -1551,7 +1555,7 @@
"x-appwrite": {
"method": "createMfaRecoveryCodes",
"group": "mfa",
"weight": 293,
"weight": 258,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-recovery-codes.md",
@@ -1652,7 +1656,7 @@
"x-appwrite": {
"method": "updateMfaRecoveryCodes",
"group": "mfa",
"weight": 294,
"weight": 259,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-recovery-codes.md",
@@ -1874,7 +1878,8 @@
"type": "string",
"description": "Current user password. Must be at least 8 chars.",
"default": "",
"x-example": "password"
"x-example": "password",
"format": "password"
}
},
"required": [
@@ -1949,13 +1954,15 @@
"type": "string",
"description": "Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212.",
"default": null,
"x-example": "+12065550100"
"x-example": "+12065550100",
"format": "phone"
},
"password": {
"type": "string",
"description": "User password. Must be at least 8 chars.",
"default": null,
"x-example": "password"
"x-example": "password",
"format": "password"
}
},
"required": [
@@ -2160,13 +2167,15 @@
"type": "string",
"description": "User email.",
"default": null,
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"url": {
"type": "string",
"description": "URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.",
"default": null,
"x-example": "https:\/\/example.com"
"x-example": "https:\/\/example.com",
"format": "url"
}
},
"required": [
@@ -2485,13 +2494,15 @@
"type": "string",
"description": "User email.",
"default": null,
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"password": {
"type": "string",
"description": "User password. Must be at least 8 chars.",
"default": null,
"x-example": "password"
"x-example": "password",
"format": "password"
}
},
"required": [
@@ -2683,9 +2694,7 @@
"yammer",
"yandex",
"zoho",
"zoom",
"mock",
"mock-unverified"
"zoom"
],
"x-enum-name": "OAuthProvider",
"x-enum-keys": [],
@@ -3429,7 +3438,8 @@
"type": "string",
"description": "User email.",
"default": null,
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"phrase": {
"type": "boolean",
@@ -3520,13 +3530,15 @@
"type": "string",
"description": "User email.",
"default": null,
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"url": {
"type": "string",
"description": "URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.",
"default": "",
"x-example": "https:\/\/example.com"
"x-example": "https:\/\/example.com",
"format": "url"
},
"phrase": {
"type": "boolean",
@@ -3638,9 +3650,7 @@
"yammer",
"yandex",
"zoho",
"zoom",
"mock",
"mock-unverified"
"zoom"
],
"x-enum-name": "OAuthProvider",
"x-enum-keys": [],
@@ -3754,7 +3764,8 @@
"type": "string",
"description": "Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212.",
"default": null,
"x-example": "+12065550100"
"x-example": "+12065550100",
"format": "phone"
}
},
"required": [
@@ -3882,7 +3893,8 @@
"type": "string",
"description": "URL to redirect the user back to your app from the verification email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.",
"default": null,
"x-example": "https:\/\/example.com"
"x-example": "https:\/\/example.com",
"format": "url"
}
},
"required": [
@@ -4191,7 +4203,7 @@
"x-appwrite": {
"method": "getBrowser",
"group": null,
"weight": 50,
"weight": 264,
"cookies": false,
"type": "location",
"demo": "avatars\/get-browser.md",
@@ -4317,7 +4329,7 @@
"x-appwrite": {
"method": "getCreditCard",
"group": null,
"weight": 49,
"weight": 263,
"cookies": false,
"type": "location",
"demo": "avatars\/get-credit-card.md",
@@ -4449,7 +4461,7 @@
"x-appwrite": {
"method": "getFavicon",
"group": null,
"weight": 53,
"weight": 267,
"cookies": false,
"type": "location",
"demo": "avatars\/get-favicon.md",
@@ -4513,7 +4525,7 @@
"x-appwrite": {
"method": "getFlag",
"group": null,
"weight": 51,
"weight": 265,
"cookies": false,
"type": "location",
"demo": "avatars\/get-flag.md",
@@ -5001,7 +5013,7 @@
"x-appwrite": {
"method": "getImage",
"group": null,
"weight": 52,
"weight": 266,
"cookies": false,
"type": "location",
"demo": "avatars\/get-image.md",
@@ -5085,7 +5097,7 @@
"x-appwrite": {
"method": "getInitials",
"group": null,
"weight": 55,
"weight": 269,
"cookies": false,
"type": "location",
"demo": "avatars\/get-initials.md",
@@ -5177,7 +5189,7 @@
"x-appwrite": {
"method": "getQR",
"group": null,
"weight": 54,
"weight": 268,
"cookies": false,
"type": "location",
"demo": "avatars\/get-qr.md",
@@ -5269,7 +5281,7 @@
"x-appwrite": {
"method": "getScreenshot",
"group": null,
"weight": 56,
"weight": 270,
"cookies": false,
"type": "location",
"demo": "avatars\/get-screenshot.md",
@@ -5982,7 +5994,7 @@
"x-appwrite": {
"method": "listTransactions",
"group": "transactions",
"weight": 362,
"weight": 338,
"cookies": false,
"type": "",
"demo": "databases\/list-transactions.md",
@@ -6049,7 +6061,7 @@
"x-appwrite": {
"method": "createTransaction",
"group": "transactions",
"weight": 358,
"weight": 334,
"cookies": false,
"type": "",
"demo": "databases\/create-transaction.md",
@@ -6087,7 +6099,8 @@
"type": "integer",
"description": "Seconds before the transaction expires.",
"default": 300,
"x-example": 60
"x-example": 60,
"format": "int32"
}
}
}
@@ -6119,7 +6132,7 @@
"x-appwrite": {
"method": "getTransaction",
"group": "transactions",
"weight": 359,
"weight": 335,
"cookies": false,
"type": "",
"demo": "databases\/get-transaction.md",
@@ -6182,7 +6195,7 @@
"x-appwrite": {
"method": "updateTransaction",
"group": "transactions",
"weight": 360,
"weight": 336,
"cookies": false,
"type": "",
"demo": "databases\/update-transaction.md",
@@ -6261,7 +6274,7 @@
"x-appwrite": {
"method": "deleteTransaction",
"group": "transactions",
"weight": 361,
"weight": 337,
"cookies": false,
"type": "",
"demo": "databases\/delete-transaction.md",
@@ -6326,7 +6339,7 @@
"x-appwrite": {
"method": "createOperations",
"group": "transactions",
"weight": 363,
"weight": 339,
"cookies": false,
"type": "",
"demo": "databases\/create-operations.md",
@@ -6407,7 +6420,7 @@
"x-appwrite": {
"method": "listDocuments",
"group": "documents",
"weight": 321,
"weight": 297,
"cookies": false,
"type": "",
"demo": "databases\/list-documents.md",
@@ -6511,7 +6524,7 @@
"x-appwrite": {
"method": "createDocument",
"group": "documents",
"weight": 313,
"weight": 289,
"cookies": false,
"type": "",
"demo": "databases\/create-document.md",
@@ -6670,7 +6683,7 @@
"x-appwrite": {
"method": "getDocument",
"group": "documents",
"weight": 314,
"weight": 290,
"cookies": false,
"type": "",
"demo": "databases\/get-document.md",
@@ -6773,7 +6786,7 @@
"x-appwrite": {
"method": "upsertDocument",
"group": "documents",
"weight": 317,
"weight": 293,
"cookies": false,
"type": "",
"demo": "databases\/upsert-document.md",
@@ -6924,7 +6937,7 @@
"x-appwrite": {
"method": "updateDocument",
"group": "documents",
"weight": 315,
"weight": 291,
"cookies": false,
"type": "",
"demo": "databases\/update-document.md",
@@ -7034,7 +7047,7 @@
"x-appwrite": {
"method": "deleteDocument",
"group": "documents",
"weight": 319,
"weight": 295,
"cookies": false,
"type": "",
"demo": "databases\/delete-document.md",
@@ -7135,7 +7148,7 @@
"x-appwrite": {
"method": "decrementDocumentAttribute",
"group": "documents",
"weight": 324,
"weight": 300,
"cookies": false,
"type": "",
"demo": "databases\/decrement-document-attribute.md",
@@ -7208,13 +7221,15 @@
"type": "number",
"description": "Value to increment the attribute by. The value must be a number.",
"default": 1,
"x-example": null
"x-example": null,
"format": "float"
},
"min": {
"type": "number",
"description": "Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.",
"default": null,
"x-example": null,
"format": "float",
"x-nullable": true
},
"transactionId": {
@@ -7256,7 +7271,7 @@
"x-appwrite": {
"method": "incrementDocumentAttribute",
"group": "documents",
"weight": 323,
"weight": 299,
"cookies": false,
"type": "",
"demo": "databases\/increment-document-attribute.md",
@@ -7329,13 +7344,15 @@
"type": "number",
"description": "Value to increment the attribute by. The value must be a number.",
"default": 1,
"x-example": null
"x-example": null,
"format": "float"
},
"max": {
"type": "number",
"description": "Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.",
"default": null,
"x-example": null,
"format": "float",
"x-nullable": true
},
"transactionId": {
@@ -7375,7 +7392,7 @@
"x-appwrite": {
"method": "listExecutions",
"group": "executions",
"weight": 454,
"weight": 431,
"cookies": false,
"type": "",
"demo": "functions\/list-executions.md",
@@ -7458,7 +7475,7 @@
"x-appwrite": {
"method": "createExecution",
"group": "executions",
"weight": 452,
"weight": 429,
"cookies": false,
"type": "",
"demo": "functions\/create-execution.md",
@@ -7577,7 +7594,7 @@
"x-appwrite": {
"method": "getExecution",
"group": "executions",
"weight": 453,
"weight": 430,
"cookies": false,
"type": "",
"demo": "functions\/get-execution.md",
@@ -7649,7 +7666,7 @@
"x-appwrite": {
"method": "query",
"group": "graphql",
"weight": 225,
"weight": 190,
"cookies": false,
"type": "graphql",
"demo": "graphql\/query.md",
@@ -7724,7 +7741,7 @@
"x-appwrite": {
"method": "mutation",
"group": "graphql",
"weight": 224,
"weight": 189,
"cookies": false,
"type": "graphql",
"demo": "graphql\/mutation.md",
@@ -7797,7 +7814,7 @@
"x-appwrite": {
"method": "get",
"group": null,
"weight": 60,
"weight": 49,
"cookies": false,
"type": "",
"demo": "locale\/get.md",
@@ -7850,7 +7867,7 @@
"x-appwrite": {
"method": "listCodes",
"group": null,
"weight": 61,
"weight": 50,
"cookies": false,
"type": "",
"demo": "locale\/list-codes.md",
@@ -7903,7 +7920,7 @@
"x-appwrite": {
"method": "listContinents",
"group": null,
"weight": 65,
"weight": 54,
"cookies": false,
"type": "",
"demo": "locale\/list-continents.md",
@@ -7956,7 +7973,7 @@
"x-appwrite": {
"method": "listCountries",
"group": null,
"weight": 62,
"weight": 51,
"cookies": false,
"type": "",
"demo": "locale\/list-countries.md",
@@ -8009,7 +8026,7 @@
"x-appwrite": {
"method": "listCountriesEU",
"group": null,
"weight": 63,
"weight": 52,
"cookies": false,
"type": "",
"demo": "locale\/list-countries-eu.md",
@@ -8062,7 +8079,7 @@
"x-appwrite": {
"method": "listCountriesPhones",
"group": null,
"weight": 64,
"weight": 53,
"cookies": false,
"type": "",
"demo": "locale\/list-countries-phones.md",
@@ -8115,7 +8132,7 @@
"x-appwrite": {
"method": "listCurrencies",
"group": null,
"weight": 66,
"weight": 55,
"cookies": false,
"type": "",
"demo": "locale\/list-currencies.md",
@@ -8168,7 +8185,7 @@
"x-appwrite": {
"method": "listLanguages",
"group": null,
"weight": 67,
"weight": 56,
"cookies": false,
"type": "",
"demo": "locale\/list-languages.md",
@@ -8223,7 +8240,7 @@
"x-appwrite": {
"method": "createSubscriber",
"group": "subscribers",
"weight": 272,
"weight": 237,
"cookies": false,
"type": "",
"demo": "messaging\/create-subscriber.md",
@@ -8308,7 +8325,7 @@
"x-appwrite": {
"method": "deleteSubscriber",
"group": "subscribers",
"weight": 276,
"weight": 241,
"cookies": false,
"type": "",
"demo": "messaging\/delete-subscriber.md",
@@ -8379,7 +8396,7 @@
"x-appwrite": {
"method": "listFiles",
"group": "files",
"weight": 525,
"weight": 527,
"cookies": false,
"type": "",
"demo": "storage\/list-files.md",
@@ -8472,7 +8489,7 @@
"x-appwrite": {
"method": "createFile",
"group": "files",
"weight": 523,
"weight": 525,
"cookies": false,
"type": "upload",
"demo": "storage\/create-file.md",
@@ -8563,7 +8580,7 @@
"x-appwrite": {
"method": "getFile",
"group": "files",
"weight": 524,
"weight": 526,
"cookies": false,
"type": "",
"demo": "storage\/get-file.md",
@@ -8634,7 +8651,7 @@
"x-appwrite": {
"method": "updateFile",
"group": "files",
"weight": 526,
"weight": 528,
"cookies": false,
"type": "",
"demo": "storage\/update-file.md",
@@ -8725,7 +8742,7 @@
"x-appwrite": {
"method": "deleteFile",
"group": "files",
"weight": 527,
"weight": 529,
"cookies": false,
"type": "",
"demo": "storage\/delete-file.md",
@@ -8796,7 +8813,7 @@
"x-appwrite": {
"method": "getFileDownload",
"group": "files",
"weight": 529,
"weight": 531,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-download.md",
@@ -8876,7 +8893,7 @@
"x-appwrite": {
"method": "getFilePreview",
"group": "files",
"weight": 528,
"weight": 530,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-preview.md",
@@ -9084,7 +9101,7 @@
"x-appwrite": {
"method": "getFileView",
"group": "files",
"weight": 530,
"weight": 532,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-view.md",
@@ -9164,7 +9181,7 @@
"x-appwrite": {
"method": "listTransactions",
"group": "transactions",
"weight": 427,
"weight": 403,
"cookies": false,
"type": "",
"demo": "tablesdb\/list-transactions.md",
@@ -9234,7 +9251,7 @@
"x-appwrite": {
"method": "createTransaction",
"group": "transactions",
"weight": 423,
"weight": 399,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-transaction.md",
@@ -9275,7 +9292,8 @@
"type": "integer",
"description": "Seconds before the transaction expires.",
"default": 300,
"x-example": 60
"x-example": 60,
"format": "int32"
}
}
}
@@ -9307,7 +9325,7 @@
"x-appwrite": {
"method": "getTransaction",
"group": "transactions",
"weight": 424,
"weight": 400,
"cookies": false,
"type": "",
"demo": "tablesdb\/get-transaction.md",
@@ -9373,7 +9391,7 @@
"x-appwrite": {
"method": "updateTransaction",
"group": "transactions",
"weight": 425,
"weight": 401,
"cookies": false,
"type": "",
"demo": "tablesdb\/update-transaction.md",
@@ -9455,7 +9473,7 @@
"x-appwrite": {
"method": "deleteTransaction",
"group": "transactions",
"weight": 426,
"weight": 402,
"cookies": false,
"type": "",
"demo": "tablesdb\/delete-transaction.md",
@@ -9523,7 +9541,7 @@
"x-appwrite": {
"method": "createOperations",
"group": "transactions",
"weight": 428,
"weight": 404,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-operations.md",
@@ -9607,7 +9625,7 @@
"x-appwrite": {
"method": "listRows",
"group": "rows",
"weight": 419,
"weight": 395,
"cookies": false,
"type": "",
"demo": "tablesdb\/list-rows.md",
@@ -9710,7 +9728,7 @@
"x-appwrite": {
"method": "createRow",
"group": "rows",
"weight": 411,
"weight": 387,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-row.md",
@@ -9864,7 +9882,7 @@
"x-appwrite": {
"method": "getRow",
"group": "rows",
"weight": 412,
"weight": 388,
"cookies": false,
"type": "",
"demo": "tablesdb\/get-row.md",
@@ -9966,7 +9984,7 @@
"x-appwrite": {
"method": "upsertRow",
"group": "rows",
"weight": 415,
"weight": 391,
"cookies": false,
"type": "",
"demo": "tablesdb\/upsert-row.md",
@@ -10112,7 +10130,7 @@
"x-appwrite": {
"method": "updateRow",
"group": "rows",
"weight": 413,
"weight": 389,
"cookies": false,
"type": "",
"demo": "tablesdb\/update-row.md",
@@ -10221,7 +10239,7 @@
"x-appwrite": {
"method": "deleteRow",
"group": "rows",
"weight": 417,
"weight": 393,
"cookies": false,
"type": "",
"demo": "tablesdb\/delete-row.md",
@@ -10321,7 +10339,7 @@
"x-appwrite": {
"method": "decrementRowColumn",
"group": "rows",
"weight": 422,
"weight": 398,
"cookies": false,
"type": "",
"demo": "tablesdb\/decrement-row-column.md",
@@ -10393,13 +10411,15 @@
"type": "number",
"description": "Value to increment the column by. The value must be a number.",
"default": 1,
"x-example": null
"x-example": null,
"format": "float"
},
"min": {
"type": "number",
"description": "Minimum value for the column. If the current value is lesser than this value, an exception will be thrown.",
"default": null,
"x-example": null,
"format": "float",
"x-nullable": true
},
"transactionId": {
@@ -10441,7 +10461,7 @@
"x-appwrite": {
"method": "incrementRowColumn",
"group": "rows",
"weight": 421,
"weight": 397,
"cookies": false,
"type": "",
"demo": "tablesdb\/increment-row-column.md",
@@ -10513,13 +10533,15 @@
"type": "number",
"description": "Value to increment the column by. The value must be a number.",
"default": 1,
"x-example": null
"x-example": null,
"format": "float"
},
"max": {
"type": "number",
"description": "Maximum value for the column. If the current value is greater than this value, an error will be thrown.",
"default": null,
"x-example": null,
"format": "float",
"x-nullable": true
},
"transactionId": {
@@ -10559,7 +10581,7 @@
"x-appwrite": {
"method": "list",
"group": "teams",
"weight": 145,
"weight": 110,
"cookies": false,
"type": "",
"demo": "teams\/list.md",
@@ -10644,7 +10666,7 @@
"x-appwrite": {
"method": "create",
"group": "teams",
"weight": 144,
"weight": 109,
"cookies": false,
"type": "",
"demo": "teams\/create.md",
@@ -10735,7 +10757,7 @@
"x-appwrite": {
"method": "get",
"group": "teams",
"weight": 146,
"weight": 111,
"cookies": false,
"type": "",
"demo": "teams\/get.md",
@@ -10798,7 +10820,7 @@
"x-appwrite": {
"method": "updateName",
"group": "teams",
"weight": 148,
"weight": 113,
"cookies": false,
"type": "",
"demo": "teams\/update-name.md",
@@ -10874,7 +10896,7 @@
"x-appwrite": {
"method": "delete",
"group": "teams",
"weight": 150,
"weight": 115,
"cookies": false,
"type": "",
"demo": "teams\/delete.md",
@@ -10937,7 +10959,7 @@
"x-appwrite": {
"method": "listMemberships",
"group": "memberships",
"weight": 152,
"weight": 117,
"cookies": false,
"type": "",
"demo": "teams\/list-memberships.md",
@@ -11030,7 +11052,7 @@
"x-appwrite": {
"method": "createMembership",
"group": "memberships",
"weight": 151,
"weight": 116,
"cookies": false,
"type": "",
"demo": "teams\/create-membership.md",
@@ -11076,7 +11098,8 @@
"type": "string",
"description": "Email of the new team member.",
"default": "",
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"userId": {
"type": "string",
@@ -11088,7 +11111,8 @@
"type": "string",
"description": "Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212.",
"default": "",
"x-example": "+12065550100"
"x-example": "+12065550100",
"format": "phone"
},
"roles": {
"type": "array",
@@ -11110,7 +11134,8 @@
"type": "string",
"description": "URL to redirect the user back to your app from the invitation email. This parameter is not required when an API key is supplied. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.",
"default": "",
"x-example": "https:\/\/example.com"
"x-example": "https:\/\/example.com",
"format": "url"
},
"name": {
"type": "string",
@@ -11151,7 +11176,7 @@
"x-appwrite": {
"method": "getMembership",
"group": "memberships",
"weight": 153,
"weight": 118,
"cookies": false,
"type": "",
"demo": "teams\/get-membership.md",
@@ -11222,7 +11247,7 @@
"x-appwrite": {
"method": "updateMembership",
"group": "memberships",
"weight": 154,
"weight": 119,
"cookies": false,
"type": "",
"demo": "teams\/update-membership.md",
@@ -11316,7 +11341,7 @@
"x-appwrite": {
"method": "deleteMembership",
"group": "memberships",
"weight": 156,
"weight": 121,
"cookies": false,
"type": "",
"demo": "teams\/delete-membership.md",
@@ -11389,7 +11414,7 @@
"x-appwrite": {
"method": "updateMembershipStatus",
"group": "memberships",
"weight": 155,
"weight": 120,
"cookies": false,
"type": "",
"demo": "teams\/update-membership-status.md",
@@ -11485,7 +11510,7 @@
"x-appwrite": {
"method": "getPrefs",
"group": "teams",
"weight": 147,
"weight": 112,
"cookies": false,
"type": "",
"demo": "teams\/get-prefs.md",
@@ -11548,7 +11573,7 @@
"x-appwrite": {
"method": "updatePrefs",
"group": "teams",
"weight": 149,
"weight": 114,
"cookies": false,
"type": "",
"demo": "teams\/update-prefs.md",
@@ -13368,6 +13393,16 @@
"description": "Total number of chunks uploaded",
"x-example": 17890,
"format": "int32"
},
"encryption": {
"type": "boolean",
"description": "Whether file contents are encrypted at rest.",
"x-example": true
},
"compression": {
"type": "string",
"description": "Compression algorithm used for the file. Will be one of none, [gzip](https:\/\/en.wikipedia.org\/wiki\/Gzip), or [zstd](https:\/\/en.wikipedia.org\/wiki\/Zstd).",
"x-example": "gzip"
}
},
"required": [
@@ -13381,7 +13416,9 @@
"mimeType",
"sizeOriginal",
"chunksTotal",
"chunksUploaded"
"chunksUploaded",
"encryption",
"compression"
],
"example": {
"$id": "5e5ea5c16897e",
@@ -13396,7 +13433,9 @@
"mimeType": "image\/png",
"sizeOriginal": 17890,
"chunksTotal": 17890,
"chunksUploaded": 17890
"chunksUploaded": 17890,
"encryption": true,
"compression": "gzip"
}
},
"team": {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+154 -115
View File
@@ -191,7 +191,8 @@
"type": "string",
"description": "User email.",
"default": null,
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"password": {
"type": "string",
@@ -280,13 +281,15 @@
"type": "string",
"description": "User email.",
"default": null,
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"password": {
"type": "string",
"description": "User password. Must be at least 8 chars.",
"default": null,
"x-example": "password"
"x-example": "password",
"format": "password"
}
},
"required": [
@@ -498,7 +501,8 @@
"type": "integer",
"description": "Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds.",
"default": 900,
"x-example": 0
"x-example": 0,
"format": "int32"
}
}
}
@@ -608,7 +612,7 @@
"x-appwrite": {
"method": "updateMFA",
"group": "mfa",
"weight": 288,
"weight": 253,
"cookies": false,
"type": "",
"demo": "account\/update-mfa.md",
@@ -683,7 +687,7 @@
"x-appwrite": {
"method": "createMfaAuthenticator",
"group": "mfa",
"weight": 290,
"weight": 255,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-authenticator.md",
@@ -807,7 +811,7 @@
"x-appwrite": {
"method": "updateMfaAuthenticator",
"group": "mfa",
"weight": 291,
"weight": 256,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-authenticator.md",
@@ -948,7 +952,7 @@
"x-appwrite": {
"method": "deleteMfaAuthenticator",
"group": "mfa",
"weight": 292,
"weight": 257,
"cookies": false,
"type": "",
"demo": "account\/delete-mfa-authenticator.md",
@@ -1072,7 +1076,7 @@
"x-appwrite": {
"method": "createMfaChallenge",
"group": "mfa",
"weight": 296,
"weight": 261,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-challenge.md",
@@ -1209,7 +1213,7 @@
"x-appwrite": {
"method": "updateMfaChallenge",
"group": "mfa",
"weight": 297,
"weight": 262,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-challenge.md",
@@ -1349,7 +1353,7 @@
"x-appwrite": {
"method": "listMfaFactors",
"group": "mfa",
"weight": 289,
"weight": 254,
"cookies": false,
"type": "",
"demo": "account\/list-mfa-factors.md",
@@ -1450,7 +1454,7 @@
"x-appwrite": {
"method": "getMfaRecoveryCodes",
"group": "mfa",
"weight": 295,
"weight": 260,
"cookies": false,
"type": "",
"demo": "account\/get-mfa-recovery-codes.md",
@@ -1551,7 +1555,7 @@
"x-appwrite": {
"method": "createMfaRecoveryCodes",
"group": "mfa",
"weight": 293,
"weight": 258,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-recovery-codes.md",
@@ -1652,7 +1656,7 @@
"x-appwrite": {
"method": "updateMfaRecoveryCodes",
"group": "mfa",
"weight": 294,
"weight": 259,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-recovery-codes.md",
@@ -1874,7 +1878,8 @@
"type": "string",
"description": "Current user password. Must be at least 8 chars.",
"default": "",
"x-example": "password"
"x-example": "password",
"format": "password"
}
},
"required": [
@@ -1949,13 +1954,15 @@
"type": "string",
"description": "Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212.",
"default": null,
"x-example": "+12065550100"
"x-example": "+12065550100",
"format": "phone"
},
"password": {
"type": "string",
"description": "User password. Must be at least 8 chars.",
"default": null,
"x-example": "password"
"x-example": "password",
"format": "password"
}
},
"required": [
@@ -2160,13 +2167,15 @@
"type": "string",
"description": "User email.",
"default": null,
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"url": {
"type": "string",
"description": "URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.",
"default": null,
"x-example": "https:\/\/example.com"
"x-example": "https:\/\/example.com",
"format": "url"
}
},
"required": [
@@ -2485,13 +2494,15 @@
"type": "string",
"description": "User email.",
"default": null,
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"password": {
"type": "string",
"description": "User password. Must be at least 8 chars.",
"default": null,
"x-example": "password"
"x-example": "password",
"format": "password"
}
},
"required": [
@@ -2683,9 +2694,7 @@
"yammer",
"yandex",
"zoho",
"zoom",
"mock",
"mock-unverified"
"zoom"
],
"x-enum-name": "OAuthProvider",
"x-enum-keys": [],
@@ -3429,7 +3438,8 @@
"type": "string",
"description": "User email.",
"default": null,
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"phrase": {
"type": "boolean",
@@ -3520,13 +3530,15 @@
"type": "string",
"description": "User email.",
"default": null,
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"url": {
"type": "string",
"description": "URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.",
"default": "",
"x-example": "https:\/\/example.com"
"x-example": "https:\/\/example.com",
"format": "url"
},
"phrase": {
"type": "boolean",
@@ -3638,9 +3650,7 @@
"yammer",
"yandex",
"zoho",
"zoom",
"mock",
"mock-unverified"
"zoom"
],
"x-enum-name": "OAuthProvider",
"x-enum-keys": [],
@@ -3754,7 +3764,8 @@
"type": "string",
"description": "Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212.",
"default": null,
"x-example": "+12065550100"
"x-example": "+12065550100",
"format": "phone"
}
},
"required": [
@@ -3882,7 +3893,8 @@
"type": "string",
"description": "URL to redirect the user back to your app from the verification email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.",
"default": null,
"x-example": "https:\/\/example.com"
"x-example": "https:\/\/example.com",
"format": "url"
}
},
"required": [
@@ -4191,7 +4203,7 @@
"x-appwrite": {
"method": "getBrowser",
"group": null,
"weight": 50,
"weight": 264,
"cookies": false,
"type": "location",
"demo": "avatars\/get-browser.md",
@@ -4317,7 +4329,7 @@
"x-appwrite": {
"method": "getCreditCard",
"group": null,
"weight": 49,
"weight": 263,
"cookies": false,
"type": "location",
"demo": "avatars\/get-credit-card.md",
@@ -4449,7 +4461,7 @@
"x-appwrite": {
"method": "getFavicon",
"group": null,
"weight": 53,
"weight": 267,
"cookies": false,
"type": "location",
"demo": "avatars\/get-favicon.md",
@@ -4513,7 +4525,7 @@
"x-appwrite": {
"method": "getFlag",
"group": null,
"weight": 51,
"weight": 265,
"cookies": false,
"type": "location",
"demo": "avatars\/get-flag.md",
@@ -5001,7 +5013,7 @@
"x-appwrite": {
"method": "getImage",
"group": null,
"weight": 52,
"weight": 266,
"cookies": false,
"type": "location",
"demo": "avatars\/get-image.md",
@@ -5085,7 +5097,7 @@
"x-appwrite": {
"method": "getInitials",
"group": null,
"weight": 55,
"weight": 269,
"cookies": false,
"type": "location",
"demo": "avatars\/get-initials.md",
@@ -5177,7 +5189,7 @@
"x-appwrite": {
"method": "getQR",
"group": null,
"weight": 54,
"weight": 268,
"cookies": false,
"type": "location",
"demo": "avatars\/get-qr.md",
@@ -5269,7 +5281,7 @@
"x-appwrite": {
"method": "getScreenshot",
"group": null,
"weight": 56,
"weight": 270,
"cookies": false,
"type": "location",
"demo": "avatars\/get-screenshot.md",
@@ -5982,7 +5994,7 @@
"x-appwrite": {
"method": "listTransactions",
"group": "transactions",
"weight": 362,
"weight": 338,
"cookies": false,
"type": "",
"demo": "databases\/list-transactions.md",
@@ -6049,7 +6061,7 @@
"x-appwrite": {
"method": "createTransaction",
"group": "transactions",
"weight": 358,
"weight": 334,
"cookies": false,
"type": "",
"demo": "databases\/create-transaction.md",
@@ -6087,7 +6099,8 @@
"type": "integer",
"description": "Seconds before the transaction expires.",
"default": 300,
"x-example": 60
"x-example": 60,
"format": "int32"
}
}
}
@@ -6119,7 +6132,7 @@
"x-appwrite": {
"method": "getTransaction",
"group": "transactions",
"weight": 359,
"weight": 335,
"cookies": false,
"type": "",
"demo": "databases\/get-transaction.md",
@@ -6182,7 +6195,7 @@
"x-appwrite": {
"method": "updateTransaction",
"group": "transactions",
"weight": 360,
"weight": 336,
"cookies": false,
"type": "",
"demo": "databases\/update-transaction.md",
@@ -6261,7 +6274,7 @@
"x-appwrite": {
"method": "deleteTransaction",
"group": "transactions",
"weight": 361,
"weight": 337,
"cookies": false,
"type": "",
"demo": "databases\/delete-transaction.md",
@@ -6326,7 +6339,7 @@
"x-appwrite": {
"method": "createOperations",
"group": "transactions",
"weight": 363,
"weight": 339,
"cookies": false,
"type": "",
"demo": "databases\/create-operations.md",
@@ -6407,7 +6420,7 @@
"x-appwrite": {
"method": "listDocuments",
"group": "documents",
"weight": 321,
"weight": 297,
"cookies": false,
"type": "",
"demo": "databases\/list-documents.md",
@@ -6511,7 +6524,7 @@
"x-appwrite": {
"method": "createDocument",
"group": "documents",
"weight": 313,
"weight": 289,
"cookies": false,
"type": "",
"demo": "databases\/create-document.md",
@@ -6670,7 +6683,7 @@
"x-appwrite": {
"method": "getDocument",
"group": "documents",
"weight": 314,
"weight": 290,
"cookies": false,
"type": "",
"demo": "databases\/get-document.md",
@@ -6773,7 +6786,7 @@
"x-appwrite": {
"method": "upsertDocument",
"group": "documents",
"weight": 317,
"weight": 293,
"cookies": false,
"type": "",
"demo": "databases\/upsert-document.md",
@@ -6924,7 +6937,7 @@
"x-appwrite": {
"method": "updateDocument",
"group": "documents",
"weight": 315,
"weight": 291,
"cookies": false,
"type": "",
"demo": "databases\/update-document.md",
@@ -7034,7 +7047,7 @@
"x-appwrite": {
"method": "deleteDocument",
"group": "documents",
"weight": 319,
"weight": 295,
"cookies": false,
"type": "",
"demo": "databases\/delete-document.md",
@@ -7135,7 +7148,7 @@
"x-appwrite": {
"method": "decrementDocumentAttribute",
"group": "documents",
"weight": 324,
"weight": 300,
"cookies": false,
"type": "",
"demo": "databases\/decrement-document-attribute.md",
@@ -7208,13 +7221,15 @@
"type": "number",
"description": "Value to increment the attribute by. The value must be a number.",
"default": 1,
"x-example": null
"x-example": null,
"format": "float"
},
"min": {
"type": "number",
"description": "Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.",
"default": null,
"x-example": null,
"format": "float",
"x-nullable": true
},
"transactionId": {
@@ -7256,7 +7271,7 @@
"x-appwrite": {
"method": "incrementDocumentAttribute",
"group": "documents",
"weight": 323,
"weight": 299,
"cookies": false,
"type": "",
"demo": "databases\/increment-document-attribute.md",
@@ -7329,13 +7344,15 @@
"type": "number",
"description": "Value to increment the attribute by. The value must be a number.",
"default": 1,
"x-example": null
"x-example": null,
"format": "float"
},
"max": {
"type": "number",
"description": "Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.",
"default": null,
"x-example": null,
"format": "float",
"x-nullable": true
},
"transactionId": {
@@ -7375,7 +7392,7 @@
"x-appwrite": {
"method": "listExecutions",
"group": "executions",
"weight": 455,
"weight": 431,
"cookies": false,
"type": "",
"demo": "functions\/list-executions.md",
@@ -7458,7 +7475,7 @@
"x-appwrite": {
"method": "createExecution",
"group": "executions",
"weight": 453,
"weight": 429,
"cookies": false,
"type": "",
"demo": "functions\/create-execution.md",
@@ -7577,7 +7594,7 @@
"x-appwrite": {
"method": "getExecution",
"group": "executions",
"weight": 454,
"weight": 430,
"cookies": false,
"type": "",
"demo": "functions\/get-execution.md",
@@ -7649,7 +7666,7 @@
"x-appwrite": {
"method": "query",
"group": "graphql",
"weight": 225,
"weight": 190,
"cookies": false,
"type": "graphql",
"demo": "graphql\/query.md",
@@ -7724,7 +7741,7 @@
"x-appwrite": {
"method": "mutation",
"group": "graphql",
"weight": 224,
"weight": 189,
"cookies": false,
"type": "graphql",
"demo": "graphql\/mutation.md",
@@ -7797,7 +7814,7 @@
"x-appwrite": {
"method": "get",
"group": null,
"weight": 60,
"weight": 49,
"cookies": false,
"type": "",
"demo": "locale\/get.md",
@@ -7850,7 +7867,7 @@
"x-appwrite": {
"method": "listCodes",
"group": null,
"weight": 61,
"weight": 50,
"cookies": false,
"type": "",
"demo": "locale\/list-codes.md",
@@ -7903,7 +7920,7 @@
"x-appwrite": {
"method": "listContinents",
"group": null,
"weight": 65,
"weight": 54,
"cookies": false,
"type": "",
"demo": "locale\/list-continents.md",
@@ -7956,7 +7973,7 @@
"x-appwrite": {
"method": "listCountries",
"group": null,
"weight": 62,
"weight": 51,
"cookies": false,
"type": "",
"demo": "locale\/list-countries.md",
@@ -8009,7 +8026,7 @@
"x-appwrite": {
"method": "listCountriesEU",
"group": null,
"weight": 63,
"weight": 52,
"cookies": false,
"type": "",
"demo": "locale\/list-countries-eu.md",
@@ -8062,7 +8079,7 @@
"x-appwrite": {
"method": "listCountriesPhones",
"group": null,
"weight": 64,
"weight": 53,
"cookies": false,
"type": "",
"demo": "locale\/list-countries-phones.md",
@@ -8115,7 +8132,7 @@
"x-appwrite": {
"method": "listCurrencies",
"group": null,
"weight": 66,
"weight": 55,
"cookies": false,
"type": "",
"demo": "locale\/list-currencies.md",
@@ -8168,7 +8185,7 @@
"x-appwrite": {
"method": "listLanguages",
"group": null,
"weight": 67,
"weight": 56,
"cookies": false,
"type": "",
"demo": "locale\/list-languages.md",
@@ -8223,7 +8240,7 @@
"x-appwrite": {
"method": "createSubscriber",
"group": "subscribers",
"weight": 272,
"weight": 237,
"cookies": false,
"type": "",
"demo": "messaging\/create-subscriber.md",
@@ -8308,7 +8325,7 @@
"x-appwrite": {
"method": "deleteSubscriber",
"group": "subscribers",
"weight": 276,
"weight": 241,
"cookies": false,
"type": "",
"demo": "messaging\/delete-subscriber.md",
@@ -8379,7 +8396,7 @@
"x-appwrite": {
"method": "listFiles",
"group": "files",
"weight": 526,
"weight": 527,
"cookies": false,
"type": "",
"demo": "storage\/list-files.md",
@@ -8472,7 +8489,7 @@
"x-appwrite": {
"method": "createFile",
"group": "files",
"weight": 524,
"weight": 525,
"cookies": false,
"type": "upload",
"demo": "storage\/create-file.md",
@@ -8563,7 +8580,7 @@
"x-appwrite": {
"method": "getFile",
"group": "files",
"weight": 525,
"weight": 526,
"cookies": false,
"type": "",
"demo": "storage\/get-file.md",
@@ -8634,7 +8651,7 @@
"x-appwrite": {
"method": "updateFile",
"group": "files",
"weight": 527,
"weight": 528,
"cookies": false,
"type": "",
"demo": "storage\/update-file.md",
@@ -8725,7 +8742,7 @@
"x-appwrite": {
"method": "deleteFile",
"group": "files",
"weight": 528,
"weight": 529,
"cookies": false,
"type": "",
"demo": "storage\/delete-file.md",
@@ -8796,7 +8813,7 @@
"x-appwrite": {
"method": "getFileDownload",
"group": "files",
"weight": 530,
"weight": 531,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-download.md",
@@ -8876,7 +8893,7 @@
"x-appwrite": {
"method": "getFilePreview",
"group": "files",
"weight": 529,
"weight": 530,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-preview.md",
@@ -9084,7 +9101,7 @@
"x-appwrite": {
"method": "getFileView",
"group": "files",
"weight": 531,
"weight": 532,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-view.md",
@@ -9164,7 +9181,7 @@
"x-appwrite": {
"method": "listTransactions",
"group": "transactions",
"weight": 427,
"weight": 403,
"cookies": false,
"type": "",
"demo": "tablesdb\/list-transactions.md",
@@ -9234,7 +9251,7 @@
"x-appwrite": {
"method": "createTransaction",
"group": "transactions",
"weight": 423,
"weight": 399,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-transaction.md",
@@ -9275,7 +9292,8 @@
"type": "integer",
"description": "Seconds before the transaction expires.",
"default": 300,
"x-example": 60
"x-example": 60,
"format": "int32"
}
}
}
@@ -9307,7 +9325,7 @@
"x-appwrite": {
"method": "getTransaction",
"group": "transactions",
"weight": 424,
"weight": 400,
"cookies": false,
"type": "",
"demo": "tablesdb\/get-transaction.md",
@@ -9373,7 +9391,7 @@
"x-appwrite": {
"method": "updateTransaction",
"group": "transactions",
"weight": 425,
"weight": 401,
"cookies": false,
"type": "",
"demo": "tablesdb\/update-transaction.md",
@@ -9455,7 +9473,7 @@
"x-appwrite": {
"method": "deleteTransaction",
"group": "transactions",
"weight": 426,
"weight": 402,
"cookies": false,
"type": "",
"demo": "tablesdb\/delete-transaction.md",
@@ -9523,7 +9541,7 @@
"x-appwrite": {
"method": "createOperations",
"group": "transactions",
"weight": 428,
"weight": 404,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-operations.md",
@@ -9607,7 +9625,7 @@
"x-appwrite": {
"method": "listRows",
"group": "rows",
"weight": 419,
"weight": 395,
"cookies": false,
"type": "",
"demo": "tablesdb\/list-rows.md",
@@ -9710,7 +9728,7 @@
"x-appwrite": {
"method": "createRow",
"group": "rows",
"weight": 411,
"weight": 387,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-row.md",
@@ -9864,7 +9882,7 @@
"x-appwrite": {
"method": "getRow",
"group": "rows",
"weight": 412,
"weight": 388,
"cookies": false,
"type": "",
"demo": "tablesdb\/get-row.md",
@@ -9966,7 +9984,7 @@
"x-appwrite": {
"method": "upsertRow",
"group": "rows",
"weight": 415,
"weight": 391,
"cookies": false,
"type": "",
"demo": "tablesdb\/upsert-row.md",
@@ -10112,7 +10130,7 @@
"x-appwrite": {
"method": "updateRow",
"group": "rows",
"weight": 413,
"weight": 389,
"cookies": false,
"type": "",
"demo": "tablesdb\/update-row.md",
@@ -10221,7 +10239,7 @@
"x-appwrite": {
"method": "deleteRow",
"group": "rows",
"weight": 417,
"weight": 393,
"cookies": false,
"type": "",
"demo": "tablesdb\/delete-row.md",
@@ -10321,7 +10339,7 @@
"x-appwrite": {
"method": "decrementRowColumn",
"group": "rows",
"weight": 422,
"weight": 398,
"cookies": false,
"type": "",
"demo": "tablesdb\/decrement-row-column.md",
@@ -10393,13 +10411,15 @@
"type": "number",
"description": "Value to increment the column by. The value must be a number.",
"default": 1,
"x-example": null
"x-example": null,
"format": "float"
},
"min": {
"type": "number",
"description": "Minimum value for the column. If the current value is lesser than this value, an exception will be thrown.",
"default": null,
"x-example": null,
"format": "float",
"x-nullable": true
},
"transactionId": {
@@ -10441,7 +10461,7 @@
"x-appwrite": {
"method": "incrementRowColumn",
"group": "rows",
"weight": 421,
"weight": 397,
"cookies": false,
"type": "",
"demo": "tablesdb\/increment-row-column.md",
@@ -10513,13 +10533,15 @@
"type": "number",
"description": "Value to increment the column by. The value must be a number.",
"default": 1,
"x-example": null
"x-example": null,
"format": "float"
},
"max": {
"type": "number",
"description": "Maximum value for the column. If the current value is greater than this value, an error will be thrown.",
"default": null,
"x-example": null,
"format": "float",
"x-nullable": true
},
"transactionId": {
@@ -10559,7 +10581,7 @@
"x-appwrite": {
"method": "list",
"group": "teams",
"weight": 145,
"weight": 110,
"cookies": false,
"type": "",
"demo": "teams\/list.md",
@@ -10644,7 +10666,7 @@
"x-appwrite": {
"method": "create",
"group": "teams",
"weight": 144,
"weight": 109,
"cookies": false,
"type": "",
"demo": "teams\/create.md",
@@ -10735,7 +10757,7 @@
"x-appwrite": {
"method": "get",
"group": "teams",
"weight": 146,
"weight": 111,
"cookies": false,
"type": "",
"demo": "teams\/get.md",
@@ -10798,7 +10820,7 @@
"x-appwrite": {
"method": "updateName",
"group": "teams",
"weight": 148,
"weight": 113,
"cookies": false,
"type": "",
"demo": "teams\/update-name.md",
@@ -10874,7 +10896,7 @@
"x-appwrite": {
"method": "delete",
"group": "teams",
"weight": 150,
"weight": 115,
"cookies": false,
"type": "",
"demo": "teams\/delete.md",
@@ -10937,7 +10959,7 @@
"x-appwrite": {
"method": "listMemberships",
"group": "memberships",
"weight": 152,
"weight": 117,
"cookies": false,
"type": "",
"demo": "teams\/list-memberships.md",
@@ -11030,7 +11052,7 @@
"x-appwrite": {
"method": "createMembership",
"group": "memberships",
"weight": 151,
"weight": 116,
"cookies": false,
"type": "",
"demo": "teams\/create-membership.md",
@@ -11076,7 +11098,8 @@
"type": "string",
"description": "Email of the new team member.",
"default": "",
"x-example": "email@example.com"
"x-example": "email@example.com",
"format": "email"
},
"userId": {
"type": "string",
@@ -11088,7 +11111,8 @@
"type": "string",
"description": "Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212.",
"default": "",
"x-example": "+12065550100"
"x-example": "+12065550100",
"format": "phone"
},
"roles": {
"type": "array",
@@ -11110,7 +11134,8 @@
"type": "string",
"description": "URL to redirect the user back to your app from the invitation email. This parameter is not required when an API key is supplied. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.",
"default": "",
"x-example": "https:\/\/example.com"
"x-example": "https:\/\/example.com",
"format": "url"
},
"name": {
"type": "string",
@@ -11151,7 +11176,7 @@
"x-appwrite": {
"method": "getMembership",
"group": "memberships",
"weight": 153,
"weight": 118,
"cookies": false,
"type": "",
"demo": "teams\/get-membership.md",
@@ -11222,7 +11247,7 @@
"x-appwrite": {
"method": "updateMembership",
"group": "memberships",
"weight": 154,
"weight": 119,
"cookies": false,
"type": "",
"demo": "teams\/update-membership.md",
@@ -11316,7 +11341,7 @@
"x-appwrite": {
"method": "deleteMembership",
"group": "memberships",
"weight": 156,
"weight": 121,
"cookies": false,
"type": "",
"demo": "teams\/delete-membership.md",
@@ -11389,7 +11414,7 @@
"x-appwrite": {
"method": "updateMembershipStatus",
"group": "memberships",
"weight": 155,
"weight": 120,
"cookies": false,
"type": "",
"demo": "teams\/update-membership-status.md",
@@ -11485,7 +11510,7 @@
"x-appwrite": {
"method": "getPrefs",
"group": "teams",
"weight": 147,
"weight": 112,
"cookies": false,
"type": "",
"demo": "teams\/get-prefs.md",
@@ -11548,7 +11573,7 @@
"x-appwrite": {
"method": "updatePrefs",
"group": "teams",
"weight": 149,
"weight": 114,
"cookies": false,
"type": "",
"demo": "teams\/update-prefs.md",
@@ -13368,6 +13393,16 @@
"description": "Total number of chunks uploaded",
"x-example": 17890,
"format": "int32"
},
"encryption": {
"type": "boolean",
"description": "Whether file contents are encrypted at rest.",
"x-example": true
},
"compression": {
"type": "string",
"description": "Compression algorithm used for the file. Will be one of none, [gzip](https:\/\/en.wikipedia.org\/wiki\/Gzip), or [zstd](https:\/\/en.wikipedia.org\/wiki\/Zstd).",
"x-example": "gzip"
}
},
"required": [
@@ -13381,7 +13416,9 @@
"mimeType",
"sizeOriginal",
"chunksTotal",
"chunksUploaded"
"chunksUploaded",
"encryption",
"compression"
],
"example": {
"$id": "5e5ea5c16897e",
@@ -13396,7 +13433,9 @@
"mimeType": "image\/png",
"sizeOriginal": 17890,
"chunksTotal": 17890,
"chunksUploaded": 17890
"chunksUploaded": 17890,
"encryption": true,
"compression": "gzip"
}
},
"team": {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -3,4 +3,6 @@
use Utopia\Image\Image;
use Utopia\System\System;
Image::setResourceLimit('memory', intval(System::getEnv('_APP_IMAGES_RESOURCE_LIMIT_MEMORY', 1024*1024*64)));
if (\class_exists('Imagick')) {
Image::setResourceLimit('memory', intval(System::getEnv('_APP_IMAGES_RESOURCE_LIMIT_MEMORY', 1024*1024*64)));
}
+9
View File
@@ -1285,6 +1285,15 @@ return [
'category' => 'GraphQL',
'description' => '',
'variables' => [
[
'name' => '_APP_GRAPHQL_INTROSPECTION',
'description' => 'Enable or disable GraphQL introspection. Set to \'enabled\' to allow schema introspection, or \'disabled\' to block it. The default value is \'enabled\'.',
'introduction' => '',
'default' => 'enabled',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_GRAPHQL_MAX_BATCH_SIZE',
'description' => 'Maximum number of batched queries per request. The default value is 10.',
+109 -97
View File
@@ -33,9 +33,9 @@ use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries\Identities;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumberUtil;
use MaxMind\Db\Reader;
use Utopia\Abuse\Abuse;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Auth\Hashes\Sha;
@@ -207,10 +207,10 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr
}
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode) {
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, Authorization $authorization) {
/** @var Appwrite\Utopia\Database\Documents\User $userFromRequest */
$userFromRequest = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId));
$userFromRequest = $authorization->skip(fn () => $dbForProject->getDocument('users', $userId));
if ($userFromRequest->isEmpty()) {
throw new Exception(Exception::USER_INVALID_TOKEN);
@@ -266,7 +266,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
$detector->getDevice()
));
Authorization::setRole(Role::user($user->getId())->toString());
$authorization->addRole(Role::user($user->getId())->toString());
$session = $dbForProject->createDocument('sessions', $session
->setAttribute('$permissions', [
@@ -275,7 +275,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
Permission::delete(Role::user($user->getId())),
]));
Authorization::skip(fn () => $dbForProject->deleteDocument('tokens', $verifiedToken->getId()));
$authorization->skip(fn () => $dbForProject->deleteDocument('tokens', $verifiedToken->getId()));
$dbForProject->purgeCachedDocument('users', $user->getId());
// Magic URL + Email OTP
@@ -376,8 +376,9 @@ App::post('/v1/account')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('authorization')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Hooks $hooks) {
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Authorization $authorization, Hooks $hooks) {
$email = \strtolower($email);
if ('console' === $project->getId()) {
@@ -469,9 +470,9 @@ App::post('/v1/account')
]);
$user->removeAttribute('$sequence');
$user = Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
$user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user));
try {
$target = Authorization::skip(fn () => $dbForProject->createDocument('targets', new Document([
$target = $authorization->skip(fn () => $dbForProject->createDocument('targets', new Document([
'$permissions' => [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
@@ -497,9 +498,9 @@ App::post('/v1/account')
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
Authorization::unsetRole(Role::guests()->toString());
Authorization::setRole(Role::user($user->getId())->toString());
Authorization::setRole(Role::users()->toString());
$authorization->removeRole(Role::guests()->toString());
$authorization->addRole(Role::user($user->getId())->toString());
$authorization->addRole(Role::users()->toString());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -976,7 +977,8 @@ App::post('/v1/account/sessions/email')
->inject('store')
->inject('proofForPassword')
->inject('proofForToken')
->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) {
->inject('authorization')
->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) {
$email = \strtolower($email);
$protocol = $request->getProtocol();
@@ -1021,7 +1023,7 @@ App::post('/v1/account/sessions/email')
$detector->getDevice()
));
Authorization::setRole(Role::user($user->getId())->toString());
$authorization->addRole(Role::user($user->getId())->toString());
// Re-hash if not using recommended algo
if ($user->getAttribute('hash') !== $proofForPassword->getHash()->getName()) {
@@ -1120,7 +1122,8 @@ App::post('/v1/account/sessions/anonymous')
->inject('store')
->inject('proofForPassword')
->inject('proofForToken')
->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) {
->inject('authorization')
->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) {
$protocol = $request->getProtocol();
if ('console' === $project->getId()) {
@@ -1165,7 +1168,7 @@ App::post('/v1/account/sessions/anonymous')
'accessedAt' => DateTime::now(),
]);
$user->removeAttribute('$sequence');
Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
$user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user));
// Create session token
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
@@ -1191,7 +1194,7 @@ App::post('/v1/account/sessions/anonymous')
$detector->getDevice()
));
Authorization::setRole(Role::user($user->getId())->toString());
$authorization->addRole(Role::user($user->getId())->toString());
$session = $dbForProject->createDocument('sessions', $session->setAttribute('$permissions', [
Permission::read(Role::user($user->getId())),
@@ -1274,6 +1277,7 @@ App::post('/v1/account/sessions/token')
->inject('store')
->inject('proofForToken')
->inject('proofForCode')
->inject('authorization')
->action($createSession);
App::get('/v1/account/sessions/oauth2/:provider')
@@ -1470,7 +1474,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
->inject('store')
->inject('proofForPassword')
->inject('proofForToken')
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) use ($oauthDefaultSuccess) {
->inject('authorization')
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) use ($oauthDefaultSuccess) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$callbackBase = $protocol . '://' . $request->getHostname();
@@ -1726,7 +1731,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
]);
$user->removeAttribute('$sequence');
$userDoc = Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
$userDoc = $authorization->skip(fn () => $dbForProject->createDocument('users', $user));
$dbForProject->createDocument('targets', new Document([
'$permissions' => [
Permission::read(Role::user($user->getId())),
@@ -1744,8 +1749,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
}
}
Authorization::setRole(Role::user($user->getId())->toString());
Authorization::setRole(Role::users()->toString());
$authorization->addRole(Role::user($user->getId())->toString());
$authorization->addRole(Role::users()->toString());
if (false === $user->getAttribute('status')) { // Account is blocked
$failureRedirect(Exception::USER_BLOCKED); // User is in status blocked
@@ -1816,7 +1821,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$dbForProject->updateDocument('users', $user->getId(), $user);
Authorization::setRole(Role::user($user->getId())->toString());
$authorization->addRole(Role::user($user->getId())->toString());
$state['success'] = URLParser::parse($state['success']);
$query = URLParser::parseQuery($state['success']['query']);
@@ -1840,7 +1845,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'ip' => $request->getIP(),
]);
Authorization::setRole(Role::user($user->getId())->toString());
$authorization->addRole(Role::user($user->getId())->toString());
$token = $dbForProject->createDocument('tokens', $token
->setAttribute('$permissions', [
@@ -2077,7 +2082,8 @@ App::post('/v1/account/tokens/magic-url')
->inject('queueForMails')
->inject('proofForPassword')
->inject('platform')
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, array $platform) {
->inject('authorization')
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
@@ -2150,7 +2156,7 @@ App::post('/v1/account/tokens/magic-url')
]);
$user->removeAttribute('$sequence');
Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
$user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user));
}
$proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL);
@@ -2170,7 +2176,7 @@ App::post('/v1/account/tokens/magic-url')
'ip' => $request->getIP(),
]);
Authorization::setRole(Role::user($user->getId())->toString());
$authorization->addRole(Role::user($user->getId())->toString());
$token = $dbForProject->createDocument('tokens', $token
->setAttribute('$permissions', [
@@ -2356,7 +2362,8 @@ App::post('/v1/account/tokens/email')
->inject('queueForMails')
->inject('proofForPassword')
->inject('proofForCode')
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, ProofsCode $proofForCode) {
->inject('authorization')
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
@@ -2425,9 +2432,9 @@ App::post('/v1/account/tokens/email')
]);
$user->removeAttribute('$sequence');
$user = Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
$user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user));
try {
$target = Authorization::skip(fn () => $dbForProject->createDocument('targets', new Document([
$target = $authorization->skip(fn () => $dbForProject->createDocument('targets', new Document([
'$permissions' => [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
@@ -2465,7 +2472,7 @@ App::post('/v1/account/tokens/email')
'ip' => $request->getIP(),
]);
Authorization::setRole(Role::user($user->getId())->toString());
$authorization->addRole(Role::user($user->getId())->toString());
$token = $dbForProject->createDocument('tokens', $token
->setAttribute('$permissions', [
@@ -2662,10 +2669,11 @@ App::put('/v1/account/sessions/magic-url')
->inject('queueForMails')
->inject('store')
->inject('proofForCode')
->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode) use ($createSession) {
->inject('authorization')
->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $authorization) use ($createSession) {
$proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL);
$proofForToken->setHash(new Sha());
$createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode);
$createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $authorization);
});
App::put('/v1/account/sessions/phone')
@@ -2711,6 +2719,7 @@ App::put('/v1/account/sessions/phone')
->inject('store')
->inject('proofForToken')
->inject('proofForCode')
->inject('authorization')
->action($createSession);
App::post('/v1/account/tokens/phone')
@@ -2754,7 +2763,8 @@ App::post('/v1/account/tokens/phone')
->inject('plan')
->inject('store')
->inject('proofForCode')
->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Store $store, ProofsCode $proofForCode) {
->inject('authorization')
->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Store $store, ProofsCode $proofForCode, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@@ -2804,9 +2814,9 @@ App::post('/v1/account/tokens/phone')
]);
$user->removeAttribute('$sequence');
Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
$user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user));
try {
$target = Authorization::skip(fn () => $dbForProject->createDocument('targets', new Document([
$target = $authorization->skip(fn () => $dbForProject->createDocument('targets', new Document([
'$permissions' => [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
@@ -2852,7 +2862,7 @@ App::post('/v1/account/tokens/phone')
'ip' => $request->getIP(),
]);
Authorization::setRole(Role::user($user->getId())->toString());
$authorization->addRole(Role::user($user->getId())->toString());
$token = $dbForProject->createDocument('tokens', $token
->setAttribute('$permissions', [
@@ -2898,26 +2908,21 @@ App::post('/v1/account/tokens/phone')
->setRecipients([$phone])
->setProviderType(MESSAGE_TYPE_SMS);
if (isset($plan['authPhone'])) {
$timelimit = $timelimit('organization:{organizationId}', $plan['authPhone'], 30 * 24 * 60 * 60); // 30 days
$timelimit
->setParam('{organizationId}', $project->getAttribute('teamId'));
$helper = PhoneNumberUtil::getInstance();
try {
$countryCode = $helper->parse($phone)->getCountryCode();
$abuse = new Abuse($timelimit);
if ($abuse->check() && System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') {
$helper = PhoneNumberUtil::getInstance();
$countryCode = $helper->parse($phone)->getCountryCode();
if (!empty($countryCode)) {
$queueForStatsUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
if (!empty($countryCode)) {
$queueForStatsUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
$queueForStatsUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
} catch (NumberParseException $e) {
// Ignore invalid phone number for country code stats
}
$queueForStatsUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
}
$token->setAttribute('secret', $secret);
@@ -3243,7 +3248,8 @@ App::patch('/v1/account/email')
->inject('project')
->inject('hooks')
->inject('proofForPassword')
->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, ProofsPassword $proofForPassword) {
->inject('authorization')
->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, ProofsPassword $proofForPassword, Authorization $authorization) {
// passwordUpdate will be empty if the user has never set a password
$passwordUpdate = $user->getAttribute('passwordUpdate');
@@ -3295,7 +3301,7 @@ App::patch('/v1/account/email')
->setAttribute('passwordUpdate', DateTime::now());
}
$target = Authorization::skip(fn () => $dbForProject->findOne('targets', [
$target = $authorization->skip(fn () => $dbForProject->findOne('targets', [
Query::equal('identifier', [$email]),
]));
@@ -3311,7 +3317,7 @@ App::patch('/v1/account/email')
$oldTarget = $user->find('identifier', $oldEmail, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email)));
$authorization->skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email)));
}
$dbForProject->purgeCachedDocument('users', $user->getId());
} catch (Duplicate) {
@@ -3352,8 +3358,9 @@ App::patch('/v1/account/phone')
->inject('queueForEvents')
->inject('project')
->inject('hooks')
->inject('proofForPassword')
->action(function (string $phone, string $password, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, ProofsPassword $proofForPassword) {
->inject('proofForPassword')
->inject('authorization')
->action(function (string $phone, string $password, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, ProofsPassword $proofForPassword, Authorization $authorization) {
// passwordUpdate will be empty if the user has never set a password
$passwordUpdate = $user->getAttribute('passwordUpdate');
@@ -3368,7 +3375,7 @@ App::patch('/v1/account/phone')
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, false]);
$target = Authorization::skip(fn () => $dbForProject->findOne('targets', [
$target = $authorization->skip(fn () => $dbForProject->findOne('targets', [
Query::equal('identifier', [$phone]),
]));
@@ -3399,7 +3406,7 @@ App::patch('/v1/account/phone')
$oldTarget = $user->find('identifier', $oldPhone, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $phone)));
$authorization->skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $phone)));
}
$dbForProject->purgeCachedDocument('users', $user->getId());
} catch (Duplicate $th) {
@@ -3535,7 +3542,9 @@ App::post('/v1/account/recovery')
->inject('queueForMails')
->inject('queueForEvents')
->inject('proofForToken')
->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Mail $queueForMails, Event $queueForEvents, ProofsToken $proofForToken) {
->inject('authorization')
->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Mail $queueForMails, Event $queueForEvents, ProofsToken $proofForToken, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
}
@@ -3571,7 +3580,7 @@ App::post('/v1/account/recovery')
'ip' => $request->getIP(),
]);
Authorization::setRole(Role::user($profile->getId())->toString());
$authorization->addRole(Role::user($profile->getId())->toString());
$recovery = $dbForProject->createDocument('tokens', $recovery
->setAttribute('$permissions', [
@@ -3727,7 +3736,8 @@ App::put('/v1/account/recovery')
->inject('hooks')
->inject('proofForPassword')
->inject('proofForToken')
->action(function (string $userId, string $secret, string $password, Response $response, User $user, Database $dbForProject, Document $project, Event $queueForEvents, Hooks $hooks, ProofsPassword $proofForPassword, ProofsToken $proofForToken) {
->inject('authorization')
->action(function (string $userId, string $secret, string $password, Response $response, User $user, Database $dbForProject, Document $project, Event $queueForEvents, Hooks $hooks, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) {
/** @var Appwrite\Utopia\Database\Documents\User $profile */
$profile = $dbForProject->getDocument('users', $userId);
@@ -3741,7 +3751,7 @@ App::put('/v1/account/recovery')
throw new Exception(Exception::USER_INVALID_TOKEN);
}
Authorization::setRole(Role::user($profile->getId())->toString());
$authorization->addRole(Role::user($profile->getId())->toString());
$newPassword = $proofForPassword->hash($password);
@@ -3844,7 +3854,8 @@ App::post('/v1/account/verifications/email')
->inject('queueForEvents')
->inject('queueForMails')
->inject('proofForToken')
->action(function (string $url, Request $request, Response $response, Document $project, array $platform, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsToken $proofForToken) {
->inject('authorization')
->action(function (string $url, Request $request, Response $response, Document $project, array $platform, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsToken $proofForToken, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
@@ -3873,7 +3884,7 @@ App::post('/v1/account/verifications/email')
'ip' => $request->getIP(),
]);
Authorization::setRole(Role::user($user->getId())->toString());
$authorization->addRole(Role::user($user->getId())->toString());
$verification = $dbForProject->createDocument('tokens', $verification
->setAttribute('$permissions', [
@@ -4072,9 +4083,10 @@ App::put('/v1/account/verifications/email')
->inject('dbForProject')
->inject('queueForEvents')
->inject('proofForToken')
->action(function (string $userId, string $secret, Response $response, User $user, Database $dbForProject, Event $queueForEvents, ProofsToken $proofForToken) {
->inject('authorization')
->action(function (string $userId, string $secret, Response $response, User $user, Database $dbForProject, Event $queueForEvents, ProofsToken $proofForToken, Authorization $authorization) {
/** @var Appwrite\Utopia\Database\Documents\User $profile */
$profile = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId));
$profile = $authorization->skip(fn () => $dbForProject->getDocument('users', $userId));
if ($profile->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
@@ -4086,7 +4098,7 @@ App::put('/v1/account/verifications/email')
throw new Exception(Exception::USER_INVALID_TOKEN);
}
Authorization::setRole(Role::user($profile->getId())->toString());
$authorization->addRole(Role::user($profile->getId())->toString());
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('emailVerification', true));
@@ -4146,7 +4158,8 @@ App::post('/v1/account/verifications/phone')
->inject('queueForStatsUsage')
->inject('plan')
->inject('proofForCode')
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, ProofsCode $proofForCode) {
->inject('authorization')
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, ProofsCode $proofForCode, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@@ -4185,7 +4198,7 @@ App::post('/v1/account/verifications/phone')
'ip' => $request->getIP(),
]);
Authorization::setRole(Role::user($user->getId())->toString());
$authorization->addRole(Role::user($user->getId())->toString());
$verification = $dbForProject->createDocument('tokens', $verification
->setAttribute('$permissions', [
@@ -4226,26 +4239,21 @@ App::post('/v1/account/verifications/phone')
->setRecipients([$user->getAttribute('phone')])
->setProviderType(MESSAGE_TYPE_SMS);
if (isset($plan['authPhone'])) {
$timelimit = $timelimit('organization:{organizationId}', $plan['authPhone'], 30 * 24 * 60 * 60); // 30 days
$timelimit
->setParam('{organizationId}', $project->getAttribute('teamId'));
$helper = PhoneNumberUtil::getInstance();
try {
$countryCode = $helper->parse($phone)->getCountryCode();
$abuse = new Abuse($timelimit);
if ($abuse->check() && System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') {
$helper = PhoneNumberUtil::getInstance();
$countryCode = $helper->parse($phone)->getCountryCode();
if (!empty($countryCode)) {
$queueForStatsUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
if (!empty($countryCode)) {
$queueForStatsUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
$queueForStatsUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
} catch (NumberParseException $e) {
// Ignore invalid phone number for country code stats
}
$queueForStatsUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
}
$verification->setAttribute('secret', $secret);
@@ -4291,9 +4299,10 @@ App::put('/v1/account/verifications/phone')
->inject('dbForProject')
->inject('queueForEvents')
->inject('proofForCode')
->action(function (string $userId, string $secret, Response $response, User $user, Database $dbForProject, Event $queueForEvents, ProofsCode $proofForCode) {
->inject('authorization')
->action(function (string $userId, string $secret, Response $response, User $user, Database $dbForProject, Event $queueForEvents, ProofsCode $proofForCode, Authorization $authorization) {
/** @var Appwrite\Utopia\Database\Documents\User $profile */
$profile = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId));
$profile = $authorization->skip(fn () => $dbForProject->getDocument('users', $userId));
if ($profile->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
@@ -4305,7 +4314,7 @@ App::put('/v1/account/verifications/phone')
throw new Exception(Exception::USER_INVALID_TOKEN);
}
Authorization::setRole(Role::user($profile->getId())->toString());
$authorization->addRole(Role::user($profile->getId())->toString());
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('phoneVerification', true));
@@ -4358,12 +4367,13 @@ App::post('/v1/account/targets/push')
->inject('dbForProject')
->inject('store')
->inject('proofForToken')
->action(function (string $targetId, string $identifier, string $providerId, Event $queueForEvents, User $user, Request $request, Response $response, Database $dbForProject, Store $store, ProofsToken $proofForToken) {
->inject('authorization')
->action(function (string $targetId, string $identifier, string $providerId, Event $queueForEvents, User $user, Request $request, Response $response, Database $dbForProject, Store $store, ProofsToken $proofForToken, Authorization $authorization) {
$targetId = $targetId == 'unique()' ? ID::unique() : $targetId;
$provider = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId));
$provider = $authorization->skip(fn () => $dbForProject->getDocument('providers', $providerId));
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId));
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId));
if (!$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
@@ -4438,9 +4448,10 @@ App::put('/v1/account/targets/:targetId/push')
->inject('request')
->inject('response')
->inject('dbForProject')
->action(function (string $targetId, string $identifier, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) {
->inject('authorization')
->action(function (string $targetId, string $identifier, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization) {
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId));
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId));
if ($target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
@@ -4503,8 +4514,9 @@ App::delete('/v1/account/targets/:targetId/push')
->inject('request')
->inject('response')
->inject('dbForProject')
->action(function (string $targetId, Event $queueForEvents, Delete $queueForDeletes, Document $user, Request $request, Response $response, Database $dbForProject) {
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId));
->inject('authorization')
->action(function (string $targetId, Event $queueForEvents, Delete $queueForDeletes, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization) {
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId));
if ($target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
File diff suppressed because it is too large Load Diff
+7 -3
View File
@@ -28,11 +28,12 @@ use Utopia\Validator\Text;
App::init()
->groups(['graphql'])
->inject('project')
->action(function (Document $project) {
->inject('authorization')
->action(function (Document $project, Authorization $authorization) {
if (
array_key_exists('graphql', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['graphql']
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
@@ -223,8 +224,11 @@ function execute(
$flags = DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE;
$validations = GraphQL::getStandardValidationRules();
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') {
if (System::getEnv('_APP_GRAPHQL_INTROSPECTION', 'enabled') === 'disabled') {
$validations[] = new DisableIntrospection();
}
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') {
$validations[] = new QueryComplexity($maxComplexity);
$validations[] = new QueryDepth($maxDepth);
}
File diff suppressed because it is too large Load Diff
+35 -30
View File
@@ -36,6 +36,7 @@ use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Authorization\Input;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Cursor;
@@ -1073,8 +1074,9 @@ App::get('/v1/messaging/providers')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('dbForProject')
->inject('authorization')
->inject('response')
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Response $response) {
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Authorization $authorization, Response $response) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@@ -1100,7 +1102,7 @@ App::get('/v1/messaging/providers')
}
$providerId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId));
$cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('providers', $providerId));
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Provider '{$providerId}' for the 'cursor' value not found.");
@@ -2481,8 +2483,9 @@ App::get('/v1/messaging/topics')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('dbForProject')
->inject('authorization')
->inject('response')
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Response $response) {
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Authorization $authorization, Response $response) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@@ -2508,7 +2511,7 @@ App::get('/v1/messaging/topics')
}
$topicId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
$cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Topic '{$topicId}' for the 'cursor' value not found.");
@@ -2782,29 +2785,27 @@ App::post('/v1/messaging/topics/:topicId/subscribers')
->param('targetId', '', new UID(), 'Target ID. The target ID to link to the specified Topic ID.')
->inject('queueForEvents')
->inject('dbForProject')
->inject('authorization')
->inject('response')
->action(function (string $subscriberId, string $topicId, string $targetId, Event $queueForEvents, Database $dbForProject, Response $response) {
->action(function (string $subscriberId, string $topicId, string $targetId, Event $queueForEvents, Database $dbForProject, Authorization $authorization, Response $response) {
$subscriberId = $subscriberId == 'unique()' ? ID::unique() : $subscriberId;
$topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
$topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
if ($topic->isEmpty()) {
throw new Exception(Exception::TOPIC_NOT_FOUND);
}
$validator = new Authorization('subscribe');
if (!$validator->isValid($topic->getAttribute('subscribe'))) {
throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription());
if (!$authorization->isValid(new Input('subscribe', $topic->getAttribute('subscribe')))) {
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId));
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId));
if ($target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
$user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
$user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
$subscriber = new Document([
'$id' => $subscriberId,
@@ -2837,7 +2838,7 @@ App::post('/v1/messaging/topics/:topicId/subscribers')
default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE),
};
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute(
$authorization->skip(fn () => $dbForProject->increaseDocumentAttribute(
'topics',
$topicId,
$totalAttribute,
@@ -2882,8 +2883,9 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('dbForProject')
->inject('authorization')
->inject('response')
->action(function (string $topicId, array $queries, string $search, bool $includeTotal, Database $dbForProject, Response $response) {
->action(function (string $topicId, array $queries, string $search, bool $includeTotal, Database $dbForProject, Authorization $authorization, Response $response) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@@ -2894,7 +2896,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
$queries[] = Query::search('search', $search);
}
$topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
$topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
if ($topic->isEmpty()) {
throw new Exception(Exception::TOPIC_NOT_FOUND);
@@ -2917,7 +2919,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
}
$subscriberId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('subscribers', $subscriberId));
$cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('subscribers', $subscriberId));
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Subscriber '{$subscriberId}' for the 'cursor' value not found.");
@@ -2931,10 +2933,10 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
$subscribers = batch(\array_map(function (Document $subscriber) use ($dbForProject) {
return function () use ($subscriber, $dbForProject) {
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')));
$user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
$subscribers = batch(\array_map(function (Document $subscriber) use ($dbForProject, $authorization) {
return function () use ($subscriber, $dbForProject, $authorization) {
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')));
$user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
return $subscriber
->setAttribute('target', $target)
@@ -3067,9 +3069,10 @@ App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
->param('topicId', '', new UID(), 'Topic ID. The topic ID subscribed to.')
->param('subscriberId', '', new UID(), 'Subscriber ID.')
->inject('dbForProject')
->inject('authorization')
->inject('response')
->action(function (string $topicId, string $subscriberId, Database $dbForProject, Response $response) {
$topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
->action(function (string $topicId, string $subscriberId, Database $dbForProject, Authorization $authorization, Response $response) {
$topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
if ($topic->isEmpty()) {
throw new Exception(Exception::TOPIC_NOT_FOUND);
@@ -3081,8 +3084,8 @@ App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
throw new Exception(Exception::SUBSCRIBER_NOT_FOUND);
}
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')));
$user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')));
$user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
$subscriber
->setAttribute('target', $target)
@@ -3118,9 +3121,10 @@ App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
->param('subscriberId', '', new UID(), 'Subscriber ID.')
->inject('queueForEvents')
->inject('dbForProject')
->inject('authorization')
->inject('response')
->action(function (string $topicId, string $subscriberId, Event $queueForEvents, Database $dbForProject, Response $response) {
$topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
->action(function (string $topicId, string $subscriberId, Event $queueForEvents, Database $dbForProject, Authorization $authorization, Response $response) {
$topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
if ($topic->isEmpty()) {
throw new Exception(Exception::TOPIC_NOT_FOUND);
@@ -3143,7 +3147,7 @@ App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE),
};
Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute(
$authorization->skip(fn () => $dbForProject->decreaseDocumentAttribute(
'topics',
$topicId,
$totalAttribute,
@@ -3702,8 +3706,9 @@ App::get('/v1/messaging/messages')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('dbForProject')
->inject('authorization')
->inject('response')
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Response $response) {
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Authorization $authorization, Response $response) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@@ -3729,7 +3734,7 @@ App::get('/v1/messaging/messages')
}
$messageId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('messages', $messageId));
$cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('messages', $messageId));
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Message '{$messageId}' for the 'cursor' value not found.");
+9 -5
View File
@@ -342,6 +342,7 @@ App::post('/v1/migrations/csv/imports')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->inject('platform')
->inject('deviceForFiles')
@@ -356,6 +357,7 @@ App::post('/v1/migrations/csv/imports')
Response $response,
Database $dbForProject,
Database $dbForPlatform,
Authorization $authorization,
Document $project,
array $platform,
Device $deviceForFiles,
@@ -363,7 +365,7 @@ App::post('/v1/migrations/csv/imports')
Event $queueForEvents,
Migration $queueForMigrations
) {
$bucket = Authorization::skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
$bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
if ($internalFile) {
return $dbForPlatform->getDocument('buckets', 'default');
}
@@ -374,7 +376,7 @@ App::post('/v1/migrations/csv/imports')
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$file = Authorization::skip(fn () => $internalFile ? $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $fileId) : $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
$file = $authorization->skip(fn () => $internalFile ? $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $fileId) : $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
@@ -491,6 +493,7 @@ App::post('/v1/migrations/csv/exports')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->inject('platform')
->inject('queueForEvents')
@@ -509,6 +512,7 @@ App::post('/v1/migrations/csv/exports')
Response $response,
Database $dbForProject,
Database $dbForPlatform,
Authorization $authorization,
Document $project,
array $platform,
Event $queueForEvents,
@@ -520,7 +524,7 @@ App::post('/v1/migrations/csv/exports')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'default'));
$bucket = $authorization->skip(fn () => $dbForPlatform->getDocument('buckets', 'default'));
if ($bucket->isEmpty()) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
@@ -533,12 +537,12 @@ App::post('/v1/migrations/csv/exports')
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
$collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty()) {
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
+5 -4
View File
@@ -45,9 +45,10 @@ App::get('/v1/project/usage')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('authorization')
->inject('getLogsDB')
->inject('smsRates')
->action(function (string $startDate, string $endDate, string $period, Response $response, Document $project, Database $dbForProject, callable $getLogsDB, array $smsRates) {
->action(function (string $startDate, string $endDate, string $period, Response $response, Document $project, Database $dbForProject, Authorization $authorization, callable $getLogsDB, array $smsRates) {
$stats = $total = $usage = [];
$format = 'Y-m-d 00:00:00';
$firstDay = (new DateTime($startDate))->format($format);
@@ -102,7 +103,7 @@ App::get('/v1/project/usage')
'1d' => 'Y-m-d\T00:00:00.000P',
};
Authorization::skip(function () use ($dbForProject, $dbForLogs, $firstDay, $lastDay, $period, $metrics, $limit, &$total, &$stats) {
$authorization->skip(function () use ($dbForProject, $dbForLogs, $firstDay, $lastDay, $period, $metrics, $limit, &$total, &$stats) {
foreach ($metrics['total'] as $metric) {
$db = ($metric === METRIC_FILES_IMAGES_TRANSFORMED) ? $dbForLogs : $dbForProject;
@@ -286,7 +287,7 @@ App::get('/v1/project/usage')
}, $dbForProject->find('functions'));
// This total is includes free and paid SMS usage
$authPhoneTotal = Authorization::skip(fn () => $dbForProject->sum('stats', 'value', [
$authPhoneTotal = $authorization->skip(fn () => $dbForProject->sum('stats', 'value', [
Query::equal('metric', [METRIC_AUTH_METHOD_PHONE]),
Query::equal('period', ['1d']),
Query::greaterThanEqual('time', $firstDay),
@@ -294,7 +295,7 @@ App::get('/v1/project/usage')
]));
// This estimate is only for paid SMS usage
$authPhoneMetrics = Authorization::skip(fn () => $dbForProject->find('stats', [
$authPhoneMetrics = $authorization->skip(fn () => $dbForProject->find('stats', [
Query::startsWith('metric', METRIC_AUTH_METHOD_PHONE . '.'),
Query::equal('period', ['1d']),
Query::greaterThanEqual('time', $firstDay),
+4 -3
View File
@@ -227,6 +227,7 @@ App::post('/v1/projects')
if (!$sharedTablesV2) {
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$dbForProject = new Database($adapter, $cache);
$dbForProject->setDatabase(APP_DATABASE);
if ($sharedTables) {
$dbForProject
@@ -1482,7 +1483,7 @@ App::post('/v1/projects/:projectId/keys')
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForPlatform')
@@ -1627,7 +1628,7 @@ App::put('/v1/projects/:projectId/keys/:keyId')
->param('projectId', '', new UID(), 'Project unique ID.')
->param('keyId', '', new UID(), 'Key unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForPlatform')
@@ -1728,7 +1729,7 @@ App::post('/v1/projects/:projectId/jwts')
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for JWT key. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for JWT key. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('duration', 900, new Range(0, 3600), 'Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds.', true)
->inject('response')
->inject('dbForPlatform')
+57 -47
View File
@@ -23,9 +23,9 @@ use Appwrite\Utopia\Database\Validator\Queries\Memberships;
use Appwrite\Utopia\Database\Validator\Queries\Teams;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumberUtil;
use MaxMind\Db\Reader;
use Utopia\Abuse\Abuse;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Auth\Proofs\Password;
@@ -86,22 +86,24 @@ App::post('/v1/teams')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('authorization')
->inject('queueForEvents')
->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Authorization $authorization, Event $queueForEvents) {
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$isAppUser = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
$isAppUser = User::isApp($authorization->getRoles());
$teamId = $teamId == 'unique()' ? ID::unique() : $teamId;
try {
$team = Authorization::skip(fn () => $dbForProject->createDocument('teams', new Document([
$team = $authorization->skip(fn () => $dbForProject->createDocument('teams', new Document([
'$id' => $teamId,
'$permissions' => [
Permission::read(Role::team($teamId)),
Permission::update(Role::team($teamId, 'owner')),
Permission::delete(Role::team($teamId, 'owner')),
],
'labels' => [],
'name' => $name,
'total' => ($isPrivilegedUser || $isAppUser) ? 0 : 1,
'prefs' => new \stdClass(),
@@ -431,15 +433,22 @@ App::delete('/v1/teams/:teamId')
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove team from DB');
}
// Sync delete
$deletes = new Deletes();
$deletes->deleteMemberships($getProjectDB, $team, $project);
// Async delete
if ($project->getId() === 'console') {
$queueForDeletes
->setType(DELETE_TYPE_TEAM_PROJECTS)
->setDocument($team);
->setDocument($team)
->trigger();
}
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($team);
$queueForEvents
->setParam('teamId', $team->getId())
->setPayload($response->output($team, Response::MODEL_TEAM))
@@ -491,6 +500,7 @@ App::post('/v1/teams/:teamId/memberships')
->inject('project')
->inject('user')
->inject('dbForProject')
->inject('authorization')
->inject('locale')
->inject('queueForMails')
->inject('queueForMessaging')
@@ -500,9 +510,9 @@ App::post('/v1/teams/:teamId/memberships')
->inject('plan')
->inject('proofForPassword')
->inject('proofForToken')
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Password $proofForPassword, Token $proofForToken) {
$isAppUser = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Authorization $authorization, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Password $proofForPassword, Token $proofForToken) {
$isAppUser = User::isApp($authorization->getRoles());
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
$url = htmlentities($url);
if (empty($url)) {
@@ -619,13 +629,13 @@ App::post('/v1/teams/:teamId/memberships')
]);
try {
$invitee = Authorization::skip(fn () => $dbForProject->createDocument('users', $userDocument));
$invitee = $authorization->skip(fn () => $dbForProject->createDocument('users', $userDocument));
} catch (Duplicate $th) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
}
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');
$isOwner = $authorization->hasRole('team:' . $team->getId() . '/owner');
if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server)
throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not allowed to send invitations for this team');
@@ -661,11 +671,11 @@ App::post('/v1/teams/:teamId/memberships')
]);
$membership = ($isPrivilegedUser || $isAppUser) ?
Authorization::skip(fn () => $dbForProject->createDocument('memberships', $membership)) :
$authorization->skip(fn () => $dbForProject->createDocument('memberships', $membership)) :
$dbForProject->createDocument('memberships', $membership);
if ($isPrivilegedUser || $isAppUser) {
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
$authorization->skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
}
} elseif ($membership->getAttribute('confirm') === false) {
$membership->setAttribute('secret', $proofForToken->hash($secret));
@@ -677,7 +687,7 @@ App::post('/v1/teams/:teamId/memberships')
}
$membership = ($isPrivilegedUser || $isAppUser) ?
Authorization::skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)) :
$authorization->skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)) :
$dbForProject->updateDocument('memberships', $membership->getId(), $membership);
} else {
throw new Exception(Exception::MEMBERSHIP_ALREADY_CONFIRMED);
@@ -799,26 +809,21 @@ App::post('/v1/teams/:teamId/memberships')
->setRecipients([$phone])
->setProviderType('SMS');
if (isset($plan['authPhone'])) {
$timelimit = $timelimit('organization:{organizationId}', $plan['authPhone'], 30 * 24 * 60 * 60); // 30 days
$timelimit
->setParam('{organizationId}', $project->getAttribute('teamId'));
$helper = PhoneNumberUtil::getInstance();
try {
$countryCode = $helper->parse($phone)->getCountryCode();
$abuse = new Abuse($timelimit);
if ($abuse->check() && System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') {
$helper = PhoneNumberUtil::getInstance();
$countryCode = $helper->parse($phone)->getCountryCode();
if (!empty($countryCode)) {
$queueForStatsUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
if (!empty($countryCode)) {
$queueForStatsUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
$queueForStatsUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
} catch (NumberParseException $e) {
// Ignore invalid phone number for country code stats
}
$queueForStatsUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
}
}
@@ -863,7 +868,8 @@ App::get('/v1/teams/:teamId/memberships')
->inject('response')
->inject('project')
->inject('dbForProject')
->action(function (string $teamId, array $queries, string $search, bool $includeTotal, Response $response, Document $project, Database $dbForProject) {
->inject('authorization')
->action(function (string $teamId, array $queries, string $search, bool $includeTotal, Response $response, Document $project, Database $dbForProject, Authorization $authorization) {
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
@@ -933,7 +939,7 @@ App::get('/v1/teams/:teamId/memberships')
'mfa' => $project->getAttribute('auths', [])['membershipsMfa'] ?? true,
];
$roles = Authorization::getRoles();
$roles = $authorization->getRoles();
$isPrivilegedUser = User::isPrivileged($roles);
$isAppUser = User::isApp($roles);
@@ -1004,7 +1010,8 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
->inject('response')
->inject('project')
->inject('dbForProject')
->action(function (string $teamId, string $membershipId, Response $response, Document $project, Database $dbForProject) {
->inject('authorization')
->action(function (string $teamId, string $membershipId, Response $response, Document $project, Database $dbForProject, Authorization $authorization) {
$team = $dbForProject->getDocument('teams', $teamId);
@@ -1024,7 +1031,7 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
'mfa' => $project->getAttribute('auths', [])['membershipsMfa'] ?? true,
];
$roles = Authorization::getRoles();
$roles = $authorization->getRoles();
$isPrivilegedUser = User::isPrivileged($roles);
$isAppUser = User::isApp($roles);
@@ -1103,8 +1110,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('authorization')
->inject('queueForEvents')
->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents) {
->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Authorization $authorization, Event $queueForEvents) {
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
@@ -1121,9 +1129,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
throw new Exception(Exception::USER_NOT_FOUND);
}
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$isAppUser = User::isApp(Authorization::getRoles());
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
$isAppUser = User::isApp($authorization->getRoles());
$isOwner = $authorization->hasRole('team:' . $team->getId() . '/owner');
if ($project->getId() === 'console') {
// Quick check: fetch up to 2 owners to determine if only one exists
@@ -1204,12 +1212,13 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('authorization')
->inject('project')
->inject('geodb')
->inject('queueForEvents')
->inject('store')
->inject('proofForToken')
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) {
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) {
$protocol = $request->getProtocol();
$membership = $dbForProject->getDocument('memberships', $membershipId);
@@ -1218,7 +1227,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
throw new Exception(Exception::MEMBERSHIP_NOT_FOUND);
}
$team = Authorization::skip(fn () => $dbForProject->getDocument('teams', $teamId));
$team = $authorization->skip(fn () => $dbForProject->getDocument('teams', $teamId));
if ($team->isEmpty()) {
throw new Exception(Exception::TEAM_NOT_FOUND);
@@ -1254,11 +1263,11 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->setAttribute('confirm', true)
;
Authorization::skip(fn () => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true)));
$authorization->skip(fn () => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true)));
// Create session for the user if not logged in
if (!$hasSession) {
Authorization::setRole(Role::user($user->getId())->toString());
$authorization->addRole(Role::user($user->getId())->toString());
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
@@ -1286,7 +1295,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
$session = $dbForProject->createDocument('sessions', $session);
Authorization::setRole(Role::user($userId)->toString());
$authorization->addRole(Role::user($userId)->toString());
$encoded = $store
->setProperty('id', $user->getId())
@@ -1324,7 +1333,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
$dbForProject->purgeCachedDocument('users', $user->getId());
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
$authorization->skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
$queueForEvents
->setParam('userId', $user->getId())
@@ -1368,8 +1377,9 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
->inject('project')
->inject('response')
->inject('dbForProject')
->inject('authorization')
->inject('queueForEvents')
->action(function (string $teamId, string $membershipId, Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents) {
->action(function (string $teamId, string $membershipId, Document $user, Document $project, Response $response, Database $dbForProject, Authorization $authorization, Event $queueForEvents) {
$membership = $dbForProject->getDocument('memberships', $membershipId);
@@ -1427,7 +1437,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
$dbForProject->purgeCachedDocument('users', $profile->getId());
if ($membership->getAttribute('confirm')) { // Count only confirmed members
Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute('teams', $team->getId(), 'total', 1, 0));
$authorization->skip(fn () => $dbForProject->decreaseDocumentAttribute('teams', $team->getId(), 'total', 1, 0));
}
$queueForEvents
+3 -3
View File
@@ -2678,8 +2678,8 @@ App::get('/v1/users/usage')
->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->inject('register')
->action(function (string $range, Response $response, Database $dbForProject) {
->inject('authorization')
->action(function (string $range, Response $response, Database $dbForProject, Authorization $authorization) {
$periods = Config::getParam('usage', []);
$stats = $usage = [];
@@ -2689,7 +2689,7 @@ App::get('/v1/users/usage')
METRIC_SESSIONS,
];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
$authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $count => $metric) {
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
+32 -29
View File
@@ -76,7 +76,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 $dbForPlatform, Build $queueForBuilds, callable $getProjectDB, array $platform) {
$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, Authorization $authorization, Build $queueForBuilds, callable $getProjectDB, Request $request, array $platform) {
$errors = [];
foreach ($repositories as $repository) {
try {
@@ -87,12 +87,12 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
}
$projectId = $repository->getAttribute('projectId');
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
$dbForProject = $getProjectDB($project);
$resourceCollection = $resourceType === "function" ? 'functions' : 'sites';
$resourceId = $repository->getAttribute('resourceId');
$resource = Authorization::skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId));
$resource = $authorization->skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId));
$resourceInternalId = $resource->getSequence();
$deploymentId = ID::unique();
@@ -141,7 +141,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$latestCommentId = '';
if (!empty($providerPullRequestId) && $resource->getAttribute('providerSilentMode', false) === false) {
$latestComment = Authorization::skip(fn () => $dbForPlatform->findOne('vcsComments', [
$latestComment = $authorization->skip(fn () => $dbForPlatform->findOne('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerPullRequestId', [$providerPullRequestId]),
Query::orderDesc('$createdAt'),
@@ -180,7 +180,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
} finally {
Authorization::skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
} else {
@@ -191,7 +191,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
if (!empty($latestCommentId)) {
$teamId = $project->getAttribute('teamId', '');
$latestComment = Authorization::skip(fn () => $dbForPlatform->createDocument('vcsComments', new Document([
$latestComment = $authorization->skip(fn () => $dbForPlatform->createDocument('vcsComments', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
@@ -212,7 +212,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
}
}
} elseif (!empty($providerBranch)) {
$latestComments = Authorization::skip(fn () => $dbForPlatform->find('vcsComments', [
$latestComments = $authorization->skip(fn () => $dbForPlatform->find('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerBranch', [$providerBranch]),
Query::orderDesc('$createdAt'),
@@ -251,7 +251,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
} finally {
Authorization::skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
}
@@ -294,7 +294,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$commands[] = $resource->getAttribute('commands', '');
}
$deployment = Authorization::skip(fn () => $dbForProject->createDocument('deployments', new Document([
$deployment = $authorization->skip(fn () => $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
@@ -306,6 +306,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
'resourceType' => $resourceCollection,
'entrypoint' => $resource->getAttribute('entrypoint', ''),
'buildCommands' => \implode(' && ', $commands),
'startCommand' => $resource->getAttribute('startCommand', ''),
'buildOutput' => $resource->getAttribute('outputDirectory', ''),
'adapter' => $resource->getAttribute('adapter', ''),
'fallbackFile' => $resource->getAttribute('fallbackFile', ''),
@@ -334,7 +335,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
Authorization::skip(fn () => $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource));
$authorization->skip(fn () => $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource));
if ($resource->getCollection() === 'sites') {
$projectId = $project->getId();
@@ -344,7 +345,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$domain = ID::unique() . "." . $sitesDomain;
$ruleId = md5($domain);
$previewRuleId = $ruleId;
Authorization::skip(
$authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
@@ -377,7 +378,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$domain = "branch-{$branchPrefix}-{$resourceProjectHash}.{$sitesDomain}";
$ruleId = md5($domain);
try {
Authorization::skip(
$authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
@@ -408,7 +409,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$domain = "commit-" . substr($providerCommitHash, 0, 16) . ".{$sitesDomain}";
$ruleId = md5($domain);
try {
Authorization::skip(
$authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
@@ -460,7 +461,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
if ($lockAcquired) {
// Wrap in try/finally to ensure lock file gets deleted
try {
$rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId));
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId));
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$previewUrl = !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
@@ -472,7 +473,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment());
}
} finally {
Authorization::skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
}
@@ -1476,11 +1477,12 @@ App::post('/v1/vcs/github/events')
->inject('request')
->inject('response')
->inject('dbForPlatform')
->inject('authorization')
->inject('getProjectDB')
->inject('queueForBuilds')
->inject('platform')
->action(
function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) {
function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, Authorization $authorization, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) {
$payload = $request->getRawPayload();
$signatureRemote = $request->getHeader('x-hub-signature-256', '');
$signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', '');
@@ -1516,14 +1518,14 @@ App::post('/v1/vcs/github/events')
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
//find resourceId from relevant resources table
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::limit(100),
]));
// create new deployment only on push (not committed by us) and not when branch is created or deleted
if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchCreated && !$providerBranchDeleted) {
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $queueForBuilds, $getProjectDB, $platform);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform);
}
} elseif ($event == $github::EVENT_INSTALLATION) {
if ($parsedPayload["action"] == "deleted") {
@@ -1536,16 +1538,16 @@ App::post('/v1/vcs/github/events')
]);
foreach ($installations as $installation) {
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('installationInternalId', [$installation->getSequence()]),
Query::limit(1000)
]));
foreach ($repositories as $repository) {
Authorization::skip(fn () => $dbForPlatform->deleteDocument('repositories', $repository->getId()));
$authorization->skip(fn () => $dbForPlatform->deleteDocument('repositories', $repository->getId()));
}
Authorization::skip(fn () => $dbForPlatform->deleteDocument('installations', $installation->getId()));
$authorization->skip(fn () => $dbForPlatform->deleteDocument('installations', $installation->getId()));
}
}
} elseif ($event == $github::EVENT_PULL_REQUEST) {
@@ -1574,12 +1576,12 @@ App::post('/v1/vcs/github/events')
$providerCommitAuthor = $commitDetails["commitAuthor"] ?? '';
$providerCommitMessage = $commitDetails["commitMessage"] ?? '';
$repositories = Authorization::skip(fn () => $dbForPlatform->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, $dbForPlatform, $queueForBuilds, $getProjectDB, $platform);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform);
} elseif ($parsedPayload["action"] == "closed") {
// Allowed external contributions cleanup
@@ -1588,7 +1590,7 @@ App::post('/v1/vcs/github/events')
$external = $parsedPayload["external"] ?? true;
if ($external) {
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt')
]));
@@ -1599,7 +1601,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 () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
$repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
}
}
}
@@ -1786,17 +1788,18 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
->inject('response')
->inject('project')
->inject('dbForPlatform')
->inject('authorization')
->inject('getProjectDB')
->inject('queueForBuilds')
->inject('platform')
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Response $response, Document $project, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) {
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForPlatform, Authorization $authorization, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$repository = Authorization::skip(fn () => $dbForPlatform->findOne('repositories', [
$repository = $authorization->skip(fn () => $dbForPlatform->findOne('repositories', [
Query::equal('$id', [$repositoryId]),
Query::equal('projectInternalId', [$project->getSequence()])
]));
@@ -1814,7 +1817,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
// TODO: Delete from array when PR is closed
$repository = Authorization::skip(fn () => $dbForPlatform->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');
@@ -1846,7 +1849,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
$providerCommitMessage = $pullRequestResponse['title'] ?? '';
$providerCommitUrl = $pullRequestResponse['html_url'] ?? '';
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, true, $dbForPlatform, $queueForBuilds, $getProjectDB, $platform);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, '', '', '', '', $providerCommitHash, '', '', '', '', $providerPullRequestId, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform);
$response->noContent();
});
+202 -94
View File
@@ -6,6 +6,7 @@ use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\StatsUsage;
@@ -59,7 +60,7 @@ Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey)
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
{
$host = $request->getHostname() ?? '';
if (!empty($previewHostname)) {
@@ -67,16 +68,16 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
}
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$rule = Authorization::skip(function () use ($dbForPlatform, $host, $isMd5) {
if ($isMd5) {
return $dbForPlatform->getDocument('rules', md5($host));
}
return $dbForPlatform->findOne('rules', [
Query::equal('domain', [$host]),
]) ?? new Document();
});
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', md5($host)));
} else {
$rule = $authorization->skip(
fn () => $dbForPlatform->find('rules', [
Query::equal('domain', [$host]),
Query::limit(1)
])
)[0] ?? new Document();
}
$errorView = __DIR__ . '/../views/general/error.phtml';
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
@@ -111,7 +112,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
}
$projectId = $rule->getAttribute('projectId');
$project = Authorization::skip(
$project = $authorization->skip(
fn () => $dbForPlatform->getDocument('projects', $projectId)
);
@@ -119,7 +120,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
$accessedAt = $project->getAttribute('accessedAt', 0);
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));
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
}
/**
@@ -158,7 +159,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
/** @var Document $deployment */
if (!empty($rule->getAttribute('deploymentId', ''))) {
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId')));
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId')));
} else {
// 1.6.x DB schema compatibility
// TODO: Make sure deploymentId is never empty, and remove this code
@@ -172,15 +173,15 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
// Document of site or function
$resource = $resourceType === 'function' ?
Authorization::skip(fn () => $dbForProject->getDocument('functions', $resourceId)) :
Authorization::skip(fn () => $dbForProject->getDocument('sites', $resourceId));
$authorization->skip(fn () => $dbForProject->getDocument('functions', $resourceId)) :
$authorization->skip(fn () => $dbForProject->getDocument('sites', $resourceId));
// ID of active deployments
// Attempts to use attribute from both schemas (1.6 and 1.7)
$activeDeploymentId = $resource->getAttribute('deploymentId', $resource->getAttribute('deployment', ''));
// Get deployment document, as intended originally
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $activeDeploymentId));
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $activeDeploymentId));
}
if ($deployment->getAttribute('resourceType', '') === 'functions') {
@@ -199,8 +200,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
}
$resource = $type === 'function' ?
Authorization::skip(fn () => $dbForProject->getDocument('functions', $deployment->getAttribute('resourceId', ''))) :
Authorization::skip(fn () => $dbForProject->getDocument('sites', $deployment->getAttribute('resourceId', '')));
$authorization->skip(fn () => $dbForProject->getDocument('functions', $deployment->getAttribute('resourceId', ''))) :
$authorization->skip(fn () => $dbForProject->getDocument('sites', $deployment->getAttribute('resourceId', '')));
$isPreview = $type === 'function' ? false : ($rule->getAttribute('trigger', '') !== 'manual');
@@ -242,7 +243,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
$userExists = false;
$userId = $payload['userId'] ?? '';
if (!empty($userId)) {
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
$user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId));
if (!$user->isEmpty() && $user->getAttribute('status', false)) {
$userExists = true;
}
@@ -255,7 +256,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
}
$membershipExists = false;
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
if (!$project->isEmpty() && isset($user)) {
$teamId = $project->getAttribute('teamId', '');
$membership = $user->find('teamId', $teamId, 'memberships');
@@ -802,6 +803,20 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
->setProject($project)
->trigger();
/* cleanup */
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
$resourceType = $type === 'function'
? RESOURCE_TYPE_FUNCTIONS
: RESOURCE_TYPE_SITES;
$queueForDeletes
->setProject($project)
->setResourceType($resourceType)
->setResource($resource->getSequence())
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
->trigger();
}
return true;
} elseif ($type === 'api') {
return false;
@@ -812,8 +827,6 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
} else {
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown resource type ' . $type, view: $errorView);
}
return false;
}
App::init()
@@ -862,15 +875,18 @@ App::init()
->inject('devKey')
->inject('apiKey')
->inject('cors')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Func $queueForFunctions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors) {
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Func $queueForFunctions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
/*
* Appwrite Router
*/
$hostname = $request->getHostname() ?? '';
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey)) {
if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1033,7 +1049,8 @@ App::init()
->inject('dbForPlatform')
->inject('queueForCertificates')
->inject('platform')
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform) {
->inject('authorization')
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform, Authorization $authorization) {
$hostname = $request->getHostname();
$cache = Config::getParam('hostnames', []);
$platformHostnames = $platform['hostnames'] ?? [];
@@ -1061,64 +1078,64 @@ App::init()
}
// 4. Check/create rule (requires DB access)
Authorization::disable();
try {
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$document = $isMd5
? $dbForPlatform->getDocument('rules', md5($domain->get()))
: $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain->get()]),
$authorization->skip(function () use ($dbForPlatform, $domain, $console, $queueForCertificates, &$cache) {
try {
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$document = $isMd5
? $dbForPlatform->getDocument('rules', md5($domain->get()))
: $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain->get()]),
]);
if (!$document->isEmpty()) {
return;
}
// 5. Create new rule
$owner = '';
$fallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', '');
$funcDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
$siteDomain = System::getEnv('_APP_DOMAIN_SITES', '');
if (!empty($fallback) && \str_ends_with($domain->get(), $fallback)) {
$funcDomain = $fallback;
}
if (
(!empty($funcDomain) && \str_ends_with($domain->get(), $funcDomain)) ||
(!empty($siteDomain) && \str_ends_with($domain->get(), $siteDomain))
) {
$owner = 'Appwrite';
}
$ruleId = $isMd5 ? md5($domain->get()) : ID::unique();
$document = new Document([
'$id' => $ruleId,
'domain' => $domain->get(),
'type' => 'api',
'status' => 'verifying',
'projectId' => $console->getId(),
'projectInternalId' => $console->getSequence(),
'search' => implode(' ', [$ruleId, $domain->get()]),
'owner' => $owner,
'region' => $console->getAttribute('region')
]);
if (!$document->isEmpty()) {
return;
$dbForPlatform->createDocument('rules', $document);
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
$queueForCertificates
->setDomain($document)
->setSkipRenewCheck(true)
->trigger();
} catch (Duplicate $e) {
Console::info('Certificate already exists');
} finally {
$cache[$domain->get()] = true;
Config::setParam('hostnames', $cache);
}
// 5. Create new rule
$owner = '';
$fallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', '');
$funcDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
$siteDomain = System::getEnv('_APP_DOMAIN_SITES', '');
if (!empty($fallback) && \str_ends_with($domain->get(), $fallback)) {
$funcDomain = $fallback;
}
if (
(!empty($funcDomain) && \str_ends_with($domain->get(), $funcDomain)) ||
(!empty($siteDomain) && \str_ends_with($domain->get(), $siteDomain))
) {
$owner = 'Appwrite';
}
$ruleId = $isMd5 ? md5($domain->get()) : ID::unique();
$document = new Document([
'$id' => $ruleId,
'domain' => $domain->get(),
'type' => 'api',
'status' => 'verifying',
'projectId' => $console->getId(),
'projectInternalId' => $console->getSequence(),
'search' => implode(' ', [$ruleId, $domain->get()]),
'owner' => $owner,
'region' => $console->getAttribute('region')
]);
$dbForPlatform->createDocument('rules', $document);
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
$queueForCertificates
->setDomain($document)
->setSkipRenewCheck(true)
->trigger();
} catch (Duplicate $e) {
Console::info('Certificate already exists');
} finally {
$cache[$domain->get()] = true;
Config::setParam('hostnames', $cache);
Authorization::reset();
}
});
});
App::options()
@@ -1141,14 +1158,17 @@ App::options()
->inject('devKey')
->inject('apiKey')
->inject('cors')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors) {
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
/*
* Appwrite Router
*/
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1182,7 +1202,8 @@ App::error()
->inject('log')
->inject('queueForStatsUsage')
->inject('devKey')
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, StatsUsage $queueForStatsUsage) {
->inject('authorization')
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, StatsUsage $queueForStatsUsage, Document $devKey, Authorization $authorization) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$route = $utopia->getRoute();
$class = \get_class($error);
@@ -1264,7 +1285,7 @@ App::error()
* If not a publishable error, track usage stats. Publishable errors are >= 500 or those explicitly marked as publish=true in errors.php
*/
if (!$publish && $project->getId() !== 'console') {
if (!DBUser::isPrivileged(Authorization::getRoles())) {
if (!DBUser::isPrivileged($authorization->getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
@@ -1326,7 +1347,87 @@ App::error()
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
$log->addExtra('roles', Authorization::getRoles());
$log->addExtra('roles', $authorization->getRoles());
try {
/* add queries to log */
$queries = $request->getParam('queries', []);
if (!empty($queries) && is_array($queries)) {
$parsedQueries = Query::parseQueries($queries);
// format query by removing sensitive values
$formatQuery = function (array $queryArray) use (&$formatQuery): ?array {
$method = $queryArray['method'] ?? '';
$values = $queryArray['values'] ?? [];
$attribute = $queryArray['attribute'] ?? '';
if (!is_string($method) || $method === '') {
return null;
}
// logical queries - recursively format nested queries
if (in_array($method, [Query::TYPE_AND, Query::TYPE_OR], true)) {
$nested = [];
foreach ($values as $nestedArray) {
if (is_array($nestedArray)) {
$formatted = $formatQuery($nestedArray);
if ($formatted !== null) {
$nested[] = $formatted;
}
}
}
return empty($nested) ? null : [$method => $nested];
}
// select - show selected attributes
if ($method === Query::TYPE_SELECT) {
$attributes = array_values(array_filter($values, 'is_string'));
return [$method => $attributes];
}
// pagination
if (in_array($method, [
Query::TYPE_LIMIT,
Query::TYPE_OFFSET,
Query::TYPE_CURSOR_AFTER,
Query::TYPE_CURSOR_BEFORE
], true)) {
return [$method => []];
}
// orders
if (in_array($method, [
Query::TYPE_ORDER_DESC,
Query::TYPE_ORDER_ASC,
Query::TYPE_ORDER_RANDOM
], true)) {
return [$method => !empty($attribute) ? [$attribute] : []];
}
// filter
if (!empty($attribute)) {
return [$method => [$attribute]];
}
// fallback
return [$method => []];
};
$formattedQueries = [];
foreach ($parsedQueries as $query) {
$formatted = $formatQuery($query->toArray());
if ($formatted !== null) {
$formattedQueries[] = $formatted;
}
}
if (!empty($formattedQueries)) {
$log->addExtra('queries', $formattedQueries);
}
}
} catch (Throwable $_) {
// don't fail the error handler
}
$action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD';
if (!empty($sdk)) {
@@ -1402,7 +1503,7 @@ App::error()
$template = $error->getView() ?? (($route) ? $route->getLabel('error', null) : null);
// TODO: Ideally use group 'api' here, but all wildcard routes seem to have 'api' at the moment
if (!\str_starts_with($route->getPath(), '/v1')) {
if (empty($route) || !\str_starts_with($route->getPath(), '/v1')) {
$template = __DIR__ . '/../views/general/error.phtml';
}
@@ -1450,13 +1551,16 @@ App::get('/robots.txt')
->inject('platform')
->inject('previewHostname')
->inject('apiKey')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey) {
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1482,13 +1586,16 @@ App::get('/humans.txt')
->inject('platform')
->inject('previewHostname')
->inject('apiKey')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey) {
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1572,7 +1679,8 @@ App::get('/v1/ping')
->inject('project')
->inject('dbForPlatform')
->inject('queueForEvents')
->action(function (Response $response, Document $project, Database $dbForPlatform, Event $queueForEvents) {
->inject('authorization')
->action(function (Response $response, Document $project, Database $dbForPlatform, Event $queueForEvents, Authorization $authorization) {
if ($project->isEmpty() || $project->getId() === 'console') {
throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND);
}
@@ -1584,7 +1692,7 @@ App::get('/v1/ping')
->setAttribute('pingCount', $pingCount)
->setAttribute('pingedAt', $pingedAt);
Authorization::skip(function () use ($dbForPlatform, $project) {
$authorization->skip(function () use ($dbForPlatform, $project) {
$dbForPlatform->updateDocument('projects', $project->getId(), $project);
});
+1 -1
View File
@@ -191,7 +191,7 @@ App::post('/v1/mock/api-key-unprefixed')
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$scopes = array_keys(Config::getParam('scopes'));
$scopes = array_keys(Config::getParam('projectScopes'));
$key = new Document([
'$id' => ID::unique(),
+99 -228
View File
@@ -10,12 +10,12 @@ use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Realtime;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Functions\EventProcessor;
use Appwrite\SDK\Method;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
@@ -30,7 +30,7 @@ use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Queue\Publisher;
use Utopia\Database\Validator\Authorization\Input;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Validator\WhiteList;
@@ -74,151 +74,6 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
return $label;
};
/**
* This isolated event handling for `users.*.create` which is based on a `Database::EVENT_DOCUMENT_CREATE` listener may look odd, but it is **intentional**.
*
* Accounts can be created in many ways beyond `createAccount`
* (anonymous, OAuth, phone, etc.), and those flows are probably not covered in event tests; so we handle this here.
*/
$eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) {
// Only trigger events for user creation with the database listener.
if ($document->getCollection() !== 'users') {
return;
}
$queueForEvents
->setEvent('users.[userId].create')
->setParam('userId', $document->getId())
->setPayload($response->output($document, Response::MODEL_USER));
// Trigger functions, webhooks, and realtime events
$queueForFunctions
->from($queueForEvents)
->trigger();
/** Trigger webhooks events only if a project has them enabled */
if (!empty($project->getAttribute('webhooks'))) {
$queueForWebhooks
->from($queueForEvents)
->trigger();
}
/** Trigger realtime events only for non console events */
if ($queueForEvents->getProject()->getId() !== 'console') {
$queueForRealtime
->from($queueForEvents)
->trigger();
}
};
$usageDatabaseListener = function (string $event, Document $document, StatsUsage $queueForStatsUsage) {
$value = 1;
switch ($event) {
case Database::EVENT_DOCUMENT_DELETE:
$value = -1;
break;
case Database::EVENT_DOCUMENTS_DELETE:
$value = -1 * $document->getAttribute('modified', 0);
break;
case Database::EVENT_DOCUMENTS_CREATE:
$value = $document->getAttribute('modified', 0);
break;
case Database::EVENT_DOCUMENTS_UPSERT:
$value = $document->getAttribute('created', 0);
break;
}
switch (true) {
case $document->getCollection() === 'teams':
$queueForStatsUsage->addMetric(METRIC_TEAMS, $value); // per project
break;
case $document->getCollection() === 'users':
$queueForStatsUsage->addMetric(METRIC_USERS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage->addReduce($document);
}
break;
case $document->getCollection() === 'sessions': // sessions
$queueForStatsUsage->addMetric(METRIC_SESSIONS, $value); //per project
break;
case $document->getCollection() === 'databases': // databases
$queueForStatsUsage->addMetric(METRIC_DATABASES, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage->addReduce($document);
}
break;
case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$queueForStatsUsage
->addMetric(METRIC_COLLECTIONS, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value);
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage->addReduce($document);
}
break;
case str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_'): //documents
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$collectionInternalId = $parts[3] ?? 0;
$queueForStatsUsage
->addMetric(METRIC_DOCUMENTS, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), $value) // per database
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), $value); // per collection
break;
case $document->getCollection() === 'buckets': //buckets
$queueForStatsUsage
->addMetric(METRIC_BUCKETS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage
->addReduce($document);
}
break;
case str_starts_with($document->getCollection(), 'bucket_'): // files
$parts = explode('_', $document->getCollection());
$bucketInternalId = $parts[1];
$queueForStatsUsage
->addMetric(METRIC_FILES, $value) // per project
->addMetric(METRIC_FILES_STORAGE, $document->getAttribute('sizeOriginal') * $value) // per project
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES), $value) // per bucket
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES_STORAGE), $document->getAttribute('sizeOriginal') * $value); // per bucket
break;
case $document->getCollection() === 'functions':
$queueForStatsUsage
->addMetric(METRIC_FUNCTIONS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage
->addReduce($document);
}
break;
case $document->getCollection() === 'sites':
$queueForStatsUsage
->addMetric(METRIC_SITES, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage
->addReduce($document);
}
break;
case $document->getCollection() === 'deployments':
$queueForStatsUsage
->addMetric(METRIC_DEPLOYMENTS, $value) // per project
->addMetric(METRIC_DEPLOYMENTS_STORAGE, $document->getAttribute('size') * $value) // per project
->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS), $value) // per function
->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value)
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS), $value) // per function
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value);
break;
default:
break;
}
};
App::init()
->groups(['api'])
->inject('utopia')
@@ -233,7 +88,8 @@ App::init()
->inject('mode')
->inject('team')
->inject('apiKey')
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey) {
->inject('authorization')
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
$route = $utopia->getRoute();
/**
@@ -301,10 +157,6 @@ App::init()
// Step 5: API Key Authentication
if (!empty($apiKey)) {
// Verify no user session exists simultaneously
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
}
// Check if key is expired
if ($apiKey->isExpired()) {
throw new Exception(Exception::PROJECT_KEY_EXPIRED);
@@ -318,7 +170,7 @@ App::init()
// Handle special app role case
if ($apiKey->getRole() === User::ROLE_APPS) {
// Disable authorization checks for API keys
Authorization::setDefaultStatus(false);
$authorization->setDefaultStatus(false);
$user = new User([
'$id' => '',
@@ -333,23 +185,38 @@ App::init()
}
// For standard keys, update last accessed time
if ($apiKey->getType() === API_KEY_STANDARD) {
$dbKey = $project->find(
key: 'secret',
find: $request->getHeader('x-appwrite-key', ''),
subject: 'keys'
);
if (\in_array($apiKey->getType(), [API_KEY_STANDARD, API_KEY_ORGANIZATION, API_KEY_ACCOUNT])) {
$dbKey = null;
if (!empty($apiKey->getProjectId())) {
$dbKey = $project->find(
key: 'secret',
find: $request->getHeader('x-appwrite-key', ''),
subject: 'keys'
);
} elseif (!empty($apiKey->getUserId())) {
$dbKey = $user->find(
key: 'secret',
find: $request->getHeader('x-appwrite-key', ''),
subject: 'keys'
);
} elseif (!empty($apiKey->getTeamId())) {
$dbKey = $team->find(
key: 'secret',
find: $request->getHeader('x-appwrite-key', ''),
subject: 'keys'
);
}
if (!$dbKey) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$updates = new Document();
$accessedAt = $dbKey->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
$dbKey->setAttribute('accessedAt', DateTime::now());
$dbForPlatform->updateDocument('keys', $dbKey->getId(), $dbKey);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$updates->setAttribute('accessedAt', DateTime::now());
}
$sdkValidator = new WhiteList($servers, true);
@@ -360,12 +227,21 @@ App::init()
if (!in_array($sdk, $sdks)) {
$sdks[] = $sdk;
$dbKey->setAttribute('sdks', $sdks);
/** Update access time as well */
$dbKey->setAttribute('accessedAt', Datetime::now());
$dbForPlatform->updateDocument('keys', $dbKey->getId(), $dbKey);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$updates->setAttribute('sdks', $sdks);
$updates->setAttribute('accessedAt', Datetime::now());
}
}
if (!$updates->isEmpty()) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->updateDocument('keys', $dbKey->getId(), $updates));
if (!empty($apiKey->getProjectId())) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
} elseif (!empty($apiKey->getUserId())) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('users', $user->getId()));
} elseif (!empty($apiKey->getTeamId())) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('teams', $team->getId()));
}
}
@@ -392,14 +268,14 @@ App::init()
$scopes = \array_merge($scopes, $roles[$role]['scopes']);
}
Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users.
$authorization->setDefaultStatus(false); // Cancel security segmentation for admin users.
}
$scopes = \array_unique($scopes);
Authorization::setRole($role);
foreach ($user->getRoles() as $authRole) {
Authorization::setRole($authRole);
$authorization->addRole($role);
foreach ($user->getRoles($authorization) as $authRole) {
$authorization->addRole($authRole);
}
// Step 6: Update project and user last activity
@@ -407,7 +283,7 @@ App::init()
$accessedAt = $project->getAttribute('accessedAt', 0);
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));
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
}
}
@@ -442,7 +318,7 @@ App::init()
if (
array_key_exists($namespace, $project->getAttribute('services', []))
&& !$project->getAttribute('services', [])[$namespace]
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
) {
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
}
@@ -487,9 +363,6 @@ App::init()
->inject('response')
->inject('project')
->inject('user')
->inject('publisher')
->inject('publisherFunctions')
->inject('publisherWebhooks')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('queueForAudits')
@@ -499,7 +372,6 @@ App::init()
->inject('queueForStatsUsage')
->inject('queueForFunctions')
->inject('queueForMails')
->inject('queueForMigrations')
->inject('dbForProject')
->inject('timelimit')
->inject('resourceToken')
@@ -509,14 +381,15 @@ App::init()
->inject('devKey')
->inject('telemetry')
->inject('platform')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Mail $queueForMails, Migration $queueForMigrations, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform) use ($usageDatabaseListener, $eventDatabaseListener) {
->inject('authorization')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
$route = $utopia->getRoute();
if (
array_key_exists('rest', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['rest']
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
@@ -546,7 +419,7 @@ App::init()
$closestLimit = null;
$roles = Authorization::getRoles();
$roles = $authorization->getRoles();
$isPrivilegedUser = User::isPrivileged($roles);
$isAppUser = User::isApp($roles);
@@ -629,38 +502,16 @@ App::init()
$queueForBuilds->setPlatform($platform);
$queueForMails->setPlatform($platform);
// Clone the queues, to prevent events triggered by the database listener
// from overwriting the events that are supposed to be triggered in the shutdown hook.
$queueForEventsClone = new Event($publisher);
$queueForFunctions = new Func($publisherFunctions);
$queueForWebhooks = new Webhook($publisherWebhooks);
$queueForRealtime = new Realtime();
$dbForProject
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENTS_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENTS_UPSERT, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENT_CREATE, 'create-trigger-events', fn ($event, $document) => $eventDatabaseListener(
$project,
$document,
$response,
$queueForEventsClone->from($queueForEvents),
$queueForFunctions->from($queueForEvents),
$queueForWebhooks->from($queueForEvents),
$queueForRealtime->from($queueForEvents)
));
$useCache = $route->getLabel('cache', false);
$storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load');
if ($useCache) {
$route = $utopia->match($request);
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !User::isPrivileged(Authorization::getRoles());
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !User::isPrivileged($authorization->getRoles());
$key = $request->cacheIdentifier();
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
);
@@ -677,10 +528,10 @@ App::init()
if ($type === 'bucket' && (!$isImageTransformation || !$isDisabled)) {
$bucketId = $parts[1] ?? null;
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAppUser && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -691,8 +542,7 @@ App::init()
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_READ);
$valid = $validator->isValid($bucket->getRead());
$valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead()));
if (!$fileSecurity && !$valid && !$isToken) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
@@ -703,7 +553,7 @@ App::init()
if ($fileSecurity && !$valid && !$isToken) {
$file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId);
} else {
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
$file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
}
if (!$resourceToken->isEmpty() && $resourceToken->getAttribute('fileInternalId') !== $file->getSequence()) {
@@ -714,11 +564,11 @@ App::init()
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
//Do not update transformedAt if it's a console user
if (!User::isPrivileged(Authorization::getRoles())) {
if (!User::isPrivileged($authorization->getRoles())) {
$transformedAt = $file->getAttribute('transformedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
$file->setAttribute('transformedAt', DateTime::now());
Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file));
$authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file));
}
}
}
@@ -814,8 +664,10 @@ App::shutdown()
->inject('queueForWebhooks')
->inject('queueForRealtime')
->inject('dbForProject')
->inject('authorization')
->inject('timelimit')
->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, callable $timelimit) use ($parseLabel) {
->inject('eventProcessor')
->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -824,9 +676,15 @@ App::shutdown()
$queueForEvents->setPayload($responsePayload);
}
$queueForFunctions
->from($queueForEvents)
->trigger();
// Get project and function/webhook events (cached)
$functionsEvents = $eventProcessor->getFunctionsEvents($project, $dbForProject);
$webhooksEvents = $eventProcessor->getWebhooksEvents($project);
// Generate events for this operation
$generatedEvents = Event::generateEvents(
$queueForEvents->getEvent(),
$queueForEvents->getParams()
);
if ($project->getId() !== 'console') {
$queueForRealtime
@@ -834,15 +692,28 @@ App::shutdown()
->trigger();
}
/** Trigger webhooks events only if a project has them enabled
* A future optimisation is to only trigger webhooks if the webhook is "enabled"
* But it might have performance implications on the API due to the number of webhooks etc.
* Some profiling is needed to see if this is a problem.
*/
if (!empty($project->getAttribute('webhooks'))) {
$queueForWebhooks
->from($queueForEvents)
->trigger();
// Only trigger functions if there are matching function events
if (!empty($functionsEvents)) {
foreach ($generatedEvents as $event) {
if (isset($functionsEvents[$event])) {
$queueForFunctions
->from($queueForEvents)
->trigger();
break;
}
}
}
// Only trigger webhooks if there are matching webhook events
if (!empty($webhooksEvents)) {
foreach ($generatedEvents as $event) {
if (isset($webhooksEvents[$event])) {
$queueForWebhooks
->from($queueForEvents)
->trigger();
break;
}
}
}
}
@@ -976,11 +847,11 @@ App::shutdown()
$key = $request->cacheIdentifier();
$signature = md5($data['payload']);
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
$accessedAt = $cacheLog->getAttribute('accessedAt', 0);
$now = DateTime::now();
if ($cacheLog->isEmpty()) {
Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([
$authorization->skip(fn () => $dbForProject->createDocument('cache', new Document([
'$id' => $key,
'resource' => $resource,
'resourceType' => $resourceType,
@@ -990,7 +861,7 @@ App::shutdown()
])));
} elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
$cacheLog->setAttribute('accessedAt', $now);
Authorization::skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog));
$authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog));
// Overwrite the file every APP_CACHE_UPDATE seconds to update the file modified time that is used in the TTL checks in cache->load()
$cache->save($key, $data['payload']);
}
@@ -1002,7 +873,7 @@ App::shutdown()
}
if ($project->getId() !== 'console') {
if (!User::isPrivileged(Authorization::getRoles())) {
if (!User::isPrivileged($authorization->getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
+4 -3
View File
@@ -36,7 +36,8 @@ App::init()
->inject('request')
->inject('project')
->inject('geodb')
->action(function (App $utopia, Request $request, Document $project, Reader $geodb) {
->inject('authorization')
->action(function (App $utopia, Request $request, Document $project, Reader $geodb, Authorization $authorization) {
$denylist = System::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', '');
if (!empty($denylist && $project->getId() === 'console')) {
$countries = explode(',', $denylist);
@@ -49,8 +50,8 @@ App::init()
$route = $utopia->match($request);
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$isAppUser = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
$isAppUser = User::isApp($authorization->getRoles());
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
return;
+17 -11
View File
@@ -27,7 +27,6 @@ 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;
use Utopia\Pools\Group;
@@ -261,7 +260,9 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
createDatabase($app, 'getLogsDB', 'logs', $collections['logs'], $pools);
// create appwrite database, `dbForPlatform` is a direct access call.
createDatabase($app, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections) {
createDatabase($app, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections, $app) {
$authorization = $app->getResource('authorization');
if ($dbForPlatform->getCollection(AuditAdapterSQL::COLLECTION)->isEmpty()) {
$adapter = new AdapterDatabase($dbForPlatform);
$audit = new Audit($adapter);
@@ -321,9 +322,9 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
$dbForPlatform->createCollection('bucket_' . $bucket->getSequence(), $attributes, $indexes);
}
if (Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')->isEmpty())) {
if ($authorization->skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')->isEmpty())) {
Console::info(" └── Creating screenshots bucket...");
Authorization::skip(fn () => $dbForPlatform->createDocument('buckets', new Document([
$authorization->skip(fn () => $dbForPlatform->createDocument('buckets', new Document([
'$id' => ID::custom('screenshots'),
'$collection' => ID::custom('buckets'),
'name' => 'Screenshots',
@@ -338,7 +339,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
'search' => 'buckets Screenshots',
])));
$bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots'));
$bucket = $authorization->skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots'));
Console::info(" └── Creating files collection for screenshots bucket...");
$files = $collections['buckets']['files'] ?? [];
@@ -366,7 +367,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
'orders' => $index['orders'],
]), $files['indexes']);
Authorization::skip(fn () => $dbForPlatform->createCollection('bucket_' . $bucket->getSequence(), $attributes, $indexes));
$authorization->skip(fn () => $dbForPlatform->createCollection('bucket_' . $bucket->getSequence(), $attributes, $indexes));
}
});
@@ -458,8 +459,12 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
App::setResource('pools', fn () => $pools);
try {
Authorization::cleanRoles();
Authorization::setRole(Role::any()->toString());
$authorization = $app->getResource('authorization');
$request->setAuthorization($authorization);
$response->setAuthorization($authorization);
$authorization->cleanRoles();
$authorization->addRole(Role::any()->toString());
$app->run($request, $response);
} catch (\Throwable $th) {
@@ -501,7 +506,7 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
$log->addExtra('file', $th->getFile());
$log->addExtra('line', $th->getLine());
$log->addExtra('trace', $th->getTraceAsString());
$log->addExtra('roles', Authorization::getRoles());
$log->addExtra('roles', isset($authorization) ? $authorization->getRoles() : []);
$sdk = $route->getLabel("sdk", false);
@@ -560,7 +565,7 @@ $http->on(Constant::EVENT_TASK, function () use ($register, $domains) {
/** @var Utopia\Database\Database $dbForPlatform */
$dbForPlatform = $app->getResource('dbForPlatform');
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $domains, &$lastSyncUpdate) {
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $domains, &$lastSyncUpdate, $app) {
try {
$time = DateTime::now();
$limit = 1000;
@@ -577,7 +582,8 @@ $http->on(Constant::EVENT_TASK, function () use ($register, $domains) {
}
$results = [];
try {
$results = Authorization::skip(fn () => $dbForPlatform->find('rules', $queries));
$authorization = $app->getResource('authorization');
$results = $authorization->skip(fn () => $dbForPlatform->find('rules', $queries));
} catch (Throwable $th) {
Console::error($th->getMessage());
}
+3 -1
View File
@@ -22,7 +22,9 @@ Config::load('collections', __DIR__ . '/../config/collections.php', $configAdapt
Config::load('frameworks', __DIR__ . '/../config/frameworks.php', $configAdapter);
Config::load('usage', __DIR__ . '/../config/usage.php', $configAdapter);
Config::load('roles', __DIR__ . '/../config/roles.php', $configAdapter); // User roles and scopes
Config::load('scopes', __DIR__ . '/../config/scopes.php', $configAdapter); // User roles and scopes
Config::load('projectScopes', __DIR__ . '/../config/scopes/project.php', $configAdapter);
Config::load('organizationScopes', __DIR__ . '/../config/scopes/organization.php', $configAdapter);
Config::load('accountScopes', __DIR__ . '/../config/scopes/account.php', $configAdapter);
Config::load('services', __DIR__ . '/../config/services.php', $configAdapter); // List of services
Config::load('variables', __DIR__ . '/../config/variables.php', $configAdapter); // List of env variables
Config::load('regions', __DIR__ . '/../config/regions.php', $configAdapter); // List of available regions
+10 -2
View File
@@ -161,6 +161,9 @@ const ACTIVITY_TYPE_GUEST = 'guest';
const MFA_RECENT_DURATION = 1800; // 30 mins
// Database name
const APP_DATABASE = 'appwrite';
// Database Reconnect
const DATABASE_RECONNECT_SLEEP = 2;
const DATABASE_RECONNECT_MAX_ATTEMPTS = 10;
@@ -178,8 +181,10 @@ const BUILD_TYPE_DEPLOYMENT = 'deployment';
const BUILD_TYPE_RETRY = 'retry';
// Deletion Types
const DELETE_TYPE_DATABASES = 'databases';
const ENABLE_EXECUTIONS_LIMIT_ON_ROUTE = false;
const DELETE_TYPE_DATABASES = 'databases';
const DELETE_TYPE_DOCUMENT = 'document';
const DELETE_TYPE_COLLECTIONS = 'collections';
const DELETE_TYPE_TRANSACTION = 'transaction';
@@ -189,8 +194,10 @@ const DELETE_TYPE_SITES = 'sites';
const DELETE_TYPE_FUNCTIONS = 'functions';
const DELETE_TYPE_DEPLOYMENTS = 'deployments';
const DELETE_TYPE_USERS = 'users';
const DELETE_TYPE_TEAMS = 'teams';
const DELETE_TYPE_TEAM_PROJECTS = 'teams_projects';
const DELETE_TYPE_EXECUTIONS = 'executions';
const DELETE_TYPE_EXECUTIONS_LIMIT = 'executionsLimit';
const DELETE_TYPE_AUDIT = 'audit';
const DELETE_TYPE_ABUSE = 'abuse';
const DELETE_TYPE_USAGE = 'usage';
@@ -243,6 +250,8 @@ const MESSAGE_TYPE_PUSH = 'push';
// API key types
const API_KEY_STANDARD = 'standard';
const API_KEY_DYNAMIC = 'dynamic';
const API_KEY_ORGANIZATION = 'organization';
const API_KEY_ACCOUNT = 'account';
// Usage metrics
const METRIC_TEAMS = 'teams';
const METRIC_USERS = 'users';
@@ -360,7 +369,6 @@ const RESOURCE_TYPE_TOPICS = 'topics';
const RESOURCE_TYPE_SUBSCRIBERS = 'subscribers';
const RESOURCE_TYPE_MESSAGES = 'messages';
const RESOURCE_TYPE_EXECUTIONS = 'executions';
const RESOURCE_TYPE_PAYMENTS = 'payments';
// Resource types for Tokens
const TOKENS_RESOURCE_TYPE_FILES = 'files';
+54 -24
View File
@@ -4,7 +4,6 @@ use Appwrite\OpenSSL\OpenSSL;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\System\System;
Database::addFilter(
@@ -70,11 +69,11 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
$attributes = $database->find('attributes', [
$attributes = $database->getAuthorization()->skip(fn () => $database->find('attributes', [
Query::equal('collectionInternalId', [$document->getSequence()]),
Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]),
Query::limit($database->getLimitForAttributes()),
]);
]));
foreach ($attributes as $attribute) {
$attributeType = $attribute->getAttribute('type');
@@ -105,12 +104,12 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return $database
return $database->getAuthorization()->skip(fn () => $database
->find('indexes', [
Query::equal('collectionInternalId', [$document->getSequence()]),
Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]),
Query::limit($database->getLimitForIndexes()),
]);
]));
}
);
@@ -120,11 +119,11 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return $database
return $database->getAuthorization()->skip(fn () => $database
->find('platforms', [
Query::equal('projectInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]);
]));
}
);
@@ -134,12 +133,12 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return $database
return $database->getAuthorization()->skip(fn () => $database
->find('keys', [
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]);
]));
}
);
@@ -149,11 +148,11 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return $database
return $database->getAuthorization()->skip(fn () => $database
->find('devKeys', [
Query::equal('projectInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]);
]));
}
);
@@ -163,11 +162,11 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return $database
return $database->getAuthorization()->skip(fn () => $database
->find('webhooks', [
Query::equal('projectInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]);
]));
}
);
@@ -177,7 +176,7 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn () => $database->find('sessions', [
return $database->getAuthorization()->skip(fn () => $database->find('sessions', [
Query::equal('userInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
@@ -190,7 +189,7 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn () => $database
return $database->getAuthorization()->skip(fn () => $database
->find('tokens', [
Query::equal('userInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
@@ -204,7 +203,7 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn () => $database
return $database->getAuthorization()->skip(fn () => $database
->find('challenges', [
Query::equal('userInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
@@ -218,7 +217,7 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn () => $database
return $database->getAuthorization()->skip(fn () => $database
->find('authenticators', [
Query::equal('userInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
@@ -232,7 +231,7 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn () => $database
return $database->getAuthorization()->skip(fn () => $database
->find('memberships', [
Query::equal('userInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
@@ -252,14 +251,14 @@ Database::addFilter(
default => ['function', 'site']
};
return $database
return $database->getAuthorization()->skip(fn () => $database
->find('variables', [
Query::equal('resourceInternalId', [$document->getSequence()]),
Query::equal('resourceType', $resourceType),
Query::orderAsc('resourceType'),
Query::orderAsc(),
Query::limit(APP_LIMIT_SUBQUERY),
]);
]));
}
);
@@ -295,11 +294,11 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return $database
return $database->getAuthorization()->skip(fn () => $database
->find('variables', [
Query::equal('resourceType', ['project']),
Query::limit(APP_LIMIT_SUBQUERY)
]);
]));
}
);
@@ -332,7 +331,7 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn () => $database
return $database->getAuthorization()->skip(fn () => $database
->find('targets', [
Query::equal('userInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY)
@@ -346,7 +345,7 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
$targetIds = Authorization::skip(fn () => \array_map(
$targetIds = $database->getAuthorization()->skip(fn () => \array_map(
fn ($document) => $document->getAttribute('targetInternalId'),
$database->find('subscribers', [
Query::equal('topicInternalId', [$document->getSequence()]),
@@ -434,3 +433,34 @@ Database::addFilter(
return $value;
}
);
Database::addFilter(
'subQueryOrganizationKeys',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database->getAuthorization()->skip(fn () => $database
->find('keys', [
Query::equal('resourceType', ['teams']),
Query::equal('resourceInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
}
);
Database::addFilter(
'subQueryAccountKeys',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database->getAuthorization()->skip(fn () => $database
->find('keys', [
Query::equal('resourceType', ['users']),
Query::equal('resourceInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
}
);
+1 -23
View File
@@ -94,13 +94,6 @@ use Appwrite\Utopia\Response\Model\MigrationReport;
use Appwrite\Utopia\Response\Model\Mock;
use Appwrite\Utopia\Response\Model\MockNumber;
use Appwrite\Utopia\Response\Model\None;
use Appwrite\Utopia\Response\Model\PaymentFeature;
use Appwrite\Utopia\Response\Model\PaymentInvoice;
use Appwrite\Utopia\Response\Model\PaymentPlan;
use Appwrite\Utopia\Response\Model\PaymentPlanFeature;
use Appwrite\Utopia\Response\Model\PaymentProviderConfig;
use Appwrite\Utopia\Response\Model\PaymentSubscription;
use Appwrite\Utopia\Response\Model\PaymentUsageEvent;
use Appwrite\Utopia\Response\Model\Phone;
use Appwrite\Utopia\Response\Model\Platform;
use Appwrite\Utopia\Response\Model\Preferences;
@@ -184,7 +177,7 @@ Response::setModel(new BaseList('Deployments List', Response::MODEL_DEPLOYMENT_L
Response::setModel(new BaseList('Executions List', Response::MODEL_EXECUTION_LIST, 'executions', Response::MODEL_EXECUTION));
Response::setModel(new BaseList('Projects List', Response::MODEL_PROJECT_LIST, 'projects', Response::MODEL_PROJECT, true, false));
Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, false));
Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, false));
Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, true));
Response::setModel(new BaseList('Dev Keys List', Response::MODEL_DEV_KEY_LIST, 'devKeys', Response::MODEL_DEV_KEY, true, false));
Response::setModel(new BaseList('Auth Providers List', Response::MODEL_AUTH_PROVIDER_LIST, 'platforms', Response::MODEL_AUTH_PROVIDER, true, false));
Response::setModel(new BaseList('Platforms List', Response::MODEL_PLATFORM_LIST, 'platforms', Response::MODEL_PLATFORM, true, false));
@@ -347,20 +340,5 @@ Response::setModel(new Migration());
Response::setModel(new MigrationReport());
Response::setModel(new MigrationFirebaseProject());
// Payments
Response::setModel(new PaymentPlan());
Response::setModel(new PaymentFeature());
Response::setModel(new PaymentSubscription());
Response::setModel(new PaymentProviderConfig());
Response::setModel(new PaymentPlanFeature());
Response::setModel(new PaymentInvoice());
Response::setModel(new PaymentUsageEvent());
Response::setModel(new BaseList('Payment Subscription List', Response::MODEL_PAYMENT_SUBSCRIPTION_LIST, 'subscriptions', Response::MODEL_PAYMENT_SUBSCRIPTION, true, true));
Response::setModel(new BaseList('Payment Plan List', Response::MODEL_PAYMENT_PLAN_LIST, 'plans', Response::MODEL_PAYMENT_PLAN, true, true));
Response::setModel(new BaseList('Payment Feature List', Response::MODEL_PAYMENT_FEATURE_LIST, 'features', Response::MODEL_PAYMENT_FEATURE, true, true));
Response::setModel(new BaseList('Payment Plan Feature List', Response::MODEL_PAYMENT_PLAN_FEATURE_LIST, 'features', Response::MODEL_PAYMENT_PLAN_FEATURE, true, true));
Response::setModel(new BaseList('Payment Invoice List', Response::MODEL_PAYMENT_INVOICE_LIST, 'invoices', Response::MODEL_PAYMENT_INVOICE, true, true));
Response::setModel(new BaseList('Payment Usage Event List', Response::MODEL_PAYMENT_USAGE_EVENT_LIST, 'events', Response::MODEL_PAYMENT_USAGE_EVENT, true, true));
// Tests (keep last)
Response::setModel(new Mock());
+6 -5
View File
@@ -324,6 +324,12 @@ $register->set('pools', function () {
Config::setParam('pools-' . $key, $config);
}
$reconnectAttempts = (int) System::getEnv('_APP_CONNECTIONS_RECONNECT_ATTEMPTS', 5);
$reconnectSleep = (int) System::getEnv('_APP_CONNECTIONS_RECONNECT_SLEEP', 2);
$group->setReconnectAttempts($reconnectAttempts);
$group->setReconnectSleep($reconnectSleep);
return $group;
});
@@ -388,8 +394,3 @@ $register->set('promiseAdapter', function () {
$register->set('hooks', function () {
return new Hooks();
});
$register->set('registryPayments', function () {
$registry = new \Appwrite\Payments\Provider\Registry();
$registry->register('stripe', \Appwrite\Payments\Provider\StripeAdapter::class);
return $registry;
});
+341 -55
View File
@@ -15,10 +15,12 @@ use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\StatsResources;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\GraphQL\Schema;
use Appwrite\Network\Cors;
use Appwrite\Network\Platform;
@@ -82,9 +84,6 @@ App::setResource('hooks', function ($register) {
}, ['register']);
App::setResource('register', fn () => $register);
App::setResource('registryPayments', function ($register) {
return $register->get('registryPayments');
}, ['register']);
App::setResource('locale', function () {
$locale = new Locale(System::getEnv('_APP_LOCALE', 'en'));
$locale->setFallback(System::getEnv('_APP_LOCALE', 'en'));
@@ -132,6 +131,9 @@ App::setResource('queueForMails', function (Publisher $publisher) {
App::setResource('queueForBuilds', function (Publisher $publisher) {
return new Build($publisher);
}, ['publisher']);
App::setResource('queueForScreenshots', function (Publisher $publisher) {
return new Screenshot($publisher);
}, ['publisher']);
App::setResource('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
@@ -156,6 +158,9 @@ App::setResource('queueForAudits', function (Publisher $publisher) {
App::setResource('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
App::setResource('eventProcessor', function () {
return new EventProcessor();
}, []);
App::setResource('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
@@ -229,7 +234,7 @@ App::setResource('allowedSchemes', function (Document $project) {
/**
* Rule associated with a request origin.
*/
App::setResource('rule', function (Request $request, Database $dbForPlatform, Document $project) {
App::setResource('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) {
$domain = \parse_url($request->getOrigin(), PHP_URL_HOST);
if (empty($domain)) {
return new Document();
@@ -237,7 +242,7 @@ App::setResource('rule', function (Request $request, Database $dbForPlatform, Do
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$rule = Authorization::skip(function () use ($dbForPlatform, $domain, $isMd5) {
$rule = $authorization->skip(function () use ($dbForPlatform, $domain, $isMd5) {
if ($isMd5) {
return $dbForPlatform->getDocument('rules', md5($domain));
}
@@ -252,7 +257,7 @@ App::setResource('rule', function (Request $request, Database $dbForPlatform, Do
}
return $rule;
}, ['request', 'dbForPlatform', 'project']);
}, ['request', 'dbForPlatform', 'project', 'authorization']);
/**
* CORS service
@@ -320,7 +325,7 @@ App::setResource('redirectValidator', function (Document $devKey, array $allowed
return new Redirect($allowedHostnames, $allowedSchemes);
}, ['devKey', 'allowedHostnames', 'allowedSchemes']);
App::setResource('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken) {
App::setResource('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) {
/**
* Handles user authentication and session validation.
*
@@ -338,9 +343,10 @@ App::setResource('user', function (string $mode, Document $project, Document $co
* 5. Regardless of the results from steps 1-4, attempts to fetch the JWT token.
* 6. If the JWT user has a valid session ID, updates the user variable with the user from `projectDB`,
* overwriting the previous value.
* 7. If account API key is passed, use user of the account API key as long as user ID header matches too
*/
Authorization::setDefaultStatus(true);
$authorization->setDefaultStatus(true);
$store->setKey('a_session_' . $project->getId());
@@ -407,19 +413,24 @@ App::setResource('user', function (string $mode, Document $project, Document $co
}
// if (APP_MODE_ADMIN === $mode) {
// if ($user->find('teamInternalId', $project->getAttribute('teamInternalId'), 'memberships')) {
// Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users.
// $authorization->setDefaultStatus(false); // Cancel security segmentation for admin users.
// } else {
// $user = new Document([]);
// }
// }
$authJWT = $request->getHeader('x-appwrite-jwt', '');
if (!empty($authJWT) && !$project->isEmpty()) { // JWT authentication
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_JWT_AND_COOKIE_SET);
}
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
try {
$payload = $jwt->decode($authJWT);
} catch (JWTException $error) {
throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage());
}
$jwtUserId = $payload['userId'] ?? '';
if (!empty($jwtUserId)) {
if ($mode === APP_MODE_ADMIN) {
@@ -435,13 +446,41 @@ App::setResource('user', function (string $mode, Document $project, Document $co
}
}
}
// Account based on account API key
$accountKey = $request->getHeader('x-appwrite-key', '');
$accountKeyUserId = $request->getHeader('x-appwrite-user', '');
if (!empty($accountKeyUserId) && !empty($accountKey)) {
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
}
$accountKeyUser = $dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId));
if (!$accountKeyUser->isEmpty()) {
$key = $accountKeyUser->find(
key: 'secret',
find: $accountKey,
subject: 'keys'
);
if (!empty($key)) {
$expire = $key->getAttribute('expire');
if (!empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
throw new Exception(Exception::ACCOUNT_KEY_EXPIRED);
}
$user = $accountKeyUser;
}
}
}
$dbForProject->setMetadata('user', $user->getId());
$dbForPlatform->setMetadata('user', $user->getId());
return $user;
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken']);
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']);
App::setResource('project', function ($dbForPlatform, $request, $console) {
App::setResource('project', function ($dbForPlatform, $request, $console, $authorization) {
/** @var Appwrite\Utopia\Request $request */
/** @var Utopia\Database\Database $dbForPlatform */
/** @var Utopia\Database\Document $console */
@@ -452,10 +491,10 @@ App::setResource('project', function ($dbForPlatform, $request, $console) {
return $console;
}
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
return $project;
}, ['dbForPlatform', 'request', 'console']);
}, ['dbForPlatform', 'request', 'console', 'authorization']);
App::setResource('session', function (User $user, Store $store, Token $proofForToken) {
if ($user->isEmpty()) {
@@ -478,10 +517,6 @@ App::setResource('session', function (User $user, Store $store, Token $proofForT
return;
}, ['user', 'store', 'proofForToken']);
App::setResource('console', function () {
return new Document(Config::getParam('console'));
}, []);
App::setResource('store', function (): Store {
return new Store();
});
@@ -512,7 +547,15 @@ App::setResource('proofForCode', function (): Code {
return $code;
});
App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project) {
App::setResource('console', function () {
return new Document(Config::getParam('console'));
}, []);
App::setResource('authorization', function () {
return new Authorization();
}, []);
App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, StatsUsage $queueForStatsUsage, Authorization $authorization) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -528,6 +571,8 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId())
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
@@ -548,14 +593,218 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform
->setNamespace('_' . $project->getSequence());
}
/**
* This isolated event handling for `users.*.create` which is based on a `Database::EVENT_DOCUMENT_CREATE` listener may look odd, but it is **intentional**.
*
* Accounts can be created in many ways beyond `createAccount`
* (anonymous, OAuth, phone, etc.), and those flows are probably not covered in event tests; so we handle this here.
*/
$eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) {
// Only trigger events for user creation with the database listener.
if ($document->getCollection() !== 'users') {
return;
}
$queueForEvents
->setEvent('users.[userId].create')
->setParam('userId', $document->getId())
->setPayload($response->output($document, Response::MODEL_USER));
// Trigger functions, webhooks, and realtime events
$queueForFunctions
->from($queueForEvents)
->trigger();
/** Trigger webhooks events only if a project has them enabled */
if (!empty($project->getAttribute('webhooks'))) {
$queueForWebhooks
->from($queueForEvents)
->trigger();
}
/** Trigger realtime events only for non console events */
if ($queueForEvents->getProject()->getId() !== 'console') {
$queueForRealtime
->from($queueForEvents)
->trigger();
}
};
/**
* Purge function events cache when functions are created, updated or deleted.
*/
$functionsEventsCacheListener = function (string $event, Document $document, Document $project, Database $dbForProject) {
if ($document->getCollection() !== 'functions') {
return;
}
if ($project->isEmpty() || $project->getId() === 'console') {
return;
}
$hostname = $dbForProject->getAdapter()->getHostname();
$cacheKey = \sprintf(
'%s-cache-%s:%s:%s:project:%s:functions:events',
$dbForProject->getCacheName(),
$hostname ?? '',
$dbForProject->getNamespace(),
$dbForProject->getTenant(),
$project->getId()
);
$dbForProject->getCache()->purge($cacheKey);
};
$usageDatabaseListener = function (string $event, Document $document, StatsUsage $queueForStatsUsage) {
$value = 1;
switch ($event) {
case Database::EVENT_DOCUMENT_DELETE:
$value = -1;
break;
case Database::EVENT_DOCUMENTS_DELETE:
$value = -1 * $document->getAttribute('modified', 0);
break;
case Database::EVENT_DOCUMENTS_CREATE:
$value = $document->getAttribute('modified', 0);
break;
case Database::EVENT_DOCUMENTS_UPSERT:
$value = $document->getAttribute('created', 0);
break;
}
switch (true) {
case $document->getCollection() === 'teams':
$queueForStatsUsage->addMetric(METRIC_TEAMS, $value); // per project
break;
case $document->getCollection() === 'users':
$queueForStatsUsage->addMetric(METRIC_USERS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage->addReduce($document);
}
break;
case $document->getCollection() === 'sessions': // sessions
$queueForStatsUsage->addMetric(METRIC_SESSIONS, $value); //per project
break;
case $document->getCollection() === 'databases': // databases
$queueForStatsUsage->addMetric(METRIC_DATABASES, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage->addReduce($document);
}
break;
case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$queueForStatsUsage
->addMetric(METRIC_COLLECTIONS, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value);
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage->addReduce($document);
}
break;
case str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_'): //documents
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$collectionInternalId = $parts[3] ?? 0;
$queueForStatsUsage
->addMetric(METRIC_DOCUMENTS, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), $value) // per database
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), $value); // per collection
break;
case $document->getCollection() === 'buckets': //buckets
$queueForStatsUsage
->addMetric(METRIC_BUCKETS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage
->addReduce($document);
}
break;
case str_starts_with($document->getCollection(), 'bucket_'): // files
$parts = explode('_', $document->getCollection());
$bucketInternalId = $parts[1];
$queueForStatsUsage
->addMetric(METRIC_FILES, $value) // per project
->addMetric(METRIC_FILES_STORAGE, $document->getAttribute('sizeOriginal') * $value) // per project
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES), $value) // per bucket
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES_STORAGE), $document->getAttribute('sizeOriginal') * $value); // per bucket
break;
case $document->getCollection() === 'functions':
$queueForStatsUsage
->addMetric(METRIC_FUNCTIONS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage
->addReduce($document);
}
break;
case $document->getCollection() === 'sites':
$queueForStatsUsage
->addMetric(METRIC_SITES, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage
->addReduce($document);
}
break;
case $document->getCollection() === 'deployments':
$queueForStatsUsage
->addMetric(METRIC_DEPLOYMENTS, $value) // per project
->addMetric(METRIC_DEPLOYMENTS_STORAGE, $document->getAttribute('size') * $value) // per project
->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS), $value) // per function
->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value)
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS), $value) // per function
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value);
break;
default:
break;
}
};
// Clone the queues, to prevent events triggered by the database listener
// from overwriting the events that are supposed to be triggered in the shutdown hook.
$queueForEventsClone = new Event($publisher);
$queueForFunctions = new Func($publisherFunctions);
$queueForWebhooks = new Webhook($publisherWebhooks);
$queueForRealtime = new Realtime();
$database
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENTS_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENTS_UPSERT, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENT_CREATE, 'create-trigger-events', fn ($event, $document) => $eventDatabaseListener(
$project,
$document,
$response,
$queueForEventsClone->from($queueForEvents),
$queueForFunctions->from($queueForEvents),
$queueForWebhooks->from($queueForEvents),
$queueForRealtime->from($queueForEvents)
))
->on(Database::EVENT_DOCUMENT_CREATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
->on(Database::EVENT_DOCUMENT_UPDATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
->on(Database::EVENT_DOCUMENT_DELETE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
;
return $database;
}, ['pools', 'dbForPlatform', 'cache', 'project']);
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'queueForStatsUsage', 'authorization']);
App::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
App::setResource('dbForPlatform', function (Group $pools, Cache $cache) {
$adapter = new DatabasePool($pools->get('console'));
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', 'console')
@@ -565,12 +814,12 @@ App::setResource('dbForPlatform', function (Group $pools, Cache $cache) {
$database->setDocumentType('users', User::class);
return $database;
}, ['pools', 'cache']);
}, ['pools', 'cache', 'authorization']);
App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) {
$databases = [];
return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases) {
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -582,13 +831,16 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
$configure = (function (Database $database) use ($project, $dsn) {
$configure = (function (Database $database) use ($project, $dsn, $authorization) {
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId())
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
$database->setDocumentType('users', User::class);
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES)
->setDocumentType('users', User::class)
;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
@@ -618,12 +870,12 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
return $database;
};
}, ['pools', 'dbForPlatform', 'cache']);
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
App::setResource('getLogsDB', function (Group $pools, Cache $cache) {
App::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, &$database) {
return function (?Document $project = null) use ($pools, $cache, $authorization, &$database) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant((int) $project->getSequence());
return $database;
@@ -633,6 +885,8 @@ App::setResource('getLogsDB', function (Group $pools, Cache $cache) {
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
@@ -645,7 +899,7 @@ App::setResource('getLogsDB', function (Group $pools, Cache $cache) {
return $database;
};
}, ['pools', 'cache']);
}, ['pools', 'cache', 'authorization']);
App::setResource('audit', function ($dbForProject) {
$adapter = new AdapterDatabase($dbForProject);
@@ -844,7 +1098,7 @@ App::setResource('promiseAdapter', function ($register) {
return $register->get('promiseAdapter');
}, ['register']);
App::setResource('schema', function ($utopia, $dbForProject) {
App::setResource('schema', function ($utopia, $dbForProject, $authorization) {
$complexity = function (int $complexity, array $args) {
$queries = Query::parseQueries($args['queries'] ?? []);
@@ -854,8 +1108,8 @@ App::setResource('schema', function ($utopia, $dbForProject) {
return $complexity * $limit;
};
$attributes = function (int $limit, int $offset) use ($dbForProject) {
$attrs = Authorization::skip(fn () => $dbForProject->find('attributes', [
$attributes = function (int $limit, int $offset) use ($dbForProject, $authorization) {
$attrs = $authorization->skip(fn () => $dbForProject->find('attributes', [
Query::limit($limit),
Query::offset($offset),
]));
@@ -929,7 +1183,7 @@ App::setResource('schema', function ($utopia, $dbForProject) {
$urls,
$params,
);
}, ['utopia', 'dbForProject']);
}, ['utopia', 'dbForProject', 'authorization']);
App::setResource('gitHub', function (Cache $cache) {
return new VcsGitHub($cache);
@@ -957,7 +1211,7 @@ App::setResource('smsRates', function () {
return [];
});
App::setResource('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform) {
App::setResource('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) {
$devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', ''));
// Check if given key match project's development keys
@@ -976,7 +1230,7 @@ App::setResource('devKey', function (Request $request, Document $project, array
$accessedAt = $key->getAttribute('accessedAt', 0);
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
$key->setAttribute('accessedAt', DatabaseDateTime::now());
Authorization::skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key));
$authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key));
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
@@ -993,25 +1247,25 @@ App::setResource('devKey', function (Request $request, Document $project, array
/** Update access time as well */
$key->setAttribute('accessedAt', DatabaseDateTime::now());
$key = Authorization::skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key));
$key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key));
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
}
return $key;
}, ['request', 'project', 'servers', 'dbForPlatform']);
}, ['request', 'project', 'servers', 'dbForPlatform', 'authorization']);
App::setResource('team', function (Document $project, Database $dbForPlatform, App $utopia, Request $request) {
App::setResource('team', function (Document $project, Database $dbForPlatform, App $utopia, Request $request, Authorization $authorization) {
$teamInternalId = '';
if ($project->getId() !== 'console') {
$teamInternalId = $project->getAttribute('teamInternalId', '');
} else {
$route = $utopia->match($request);
$path = $route->getPath();
$path = !empty($route) ? $route->getPath() : $request->getURI();
if (str_starts_with($path, '/v1/projects/:projectId')) {
$uri = $request->getURI();
$pid = explode('/', $uri)[3];
$p = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $pid));
$p = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $pid));
$teamInternalId = $p->getAttribute('teamInternalId', '');
} elseif ($path === '/v1/projects') {
$teamId = $request->getParam('teamId', '');
@@ -1020,7 +1274,7 @@ App::setResource('team', function (Document $project, Database $dbForPlatform, A
return new Document([]);
}
$team = Authorization::skip(fn () => $dbForPlatform->getDocument('teams', $teamId));
$team = $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $teamId));
return $team;
}
}
@@ -1029,14 +1283,14 @@ App::setResource('team', function (Document $project, Database $dbForPlatform, A
return new Document([]);
}
$team = Authorization::skip(function () use ($dbForPlatform, $teamInternalId) {
$team = $authorization->skip(function () use ($dbForPlatform, $teamInternalId) {
return $dbForPlatform->findOne('teams', [
Query::equal('$sequence', [$teamInternalId]),
]);
});
return $team;
}, ['project', 'dbForPlatform', 'utopia', 'request']);
}, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization']);
App::setResource(
'isResourceBlocked',
@@ -1062,19 +1316,43 @@ App::setResource('previewHostname', function (Request $request, ?Key $apiKey) {
return '';
}, ['request', 'apiKey']);
App::setResource('apiKey', function (Request $request, Document $project): ?Key {
App::setResource('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key {
$key = $request->getHeader('x-appwrite-key');
if (empty($key)) {
return null;
}
return Key::decode($project, $key);
}, ['request', 'project']);
$key = Key::decode($project, $team, $user, $key);
$userHeader = $request->getHeader('x-appwrite-user');
$organizationHeader = $request->getHeader('x-appwrite-organization');
$projectHeader = $request->getHeader('x-appwrite-project');
if (!empty($key->getProjectId())) {
if (empty($projectHeader) || $projectHeader !== $key->getProjectId()) {
throw new Exception(Exception::PROJECT_ID_MISSING);
}
}
if (!empty($key->getUserId())) {
if (empty($userHeader) || $userHeader !== $key->getUserId()) {
throw new Exception(Exception::USER_ID_MISSING);
}
}
if (!empty($key->getTeamId())) {
if (empty($organizationHeader) || $organizationHeader !== $key->getTeamId()) {
throw new Exception(Exception::ORGANIZATION_ID_MISSING);
}
}
return $key;
}, ['request', 'project', 'team', 'user']);
App::setResource('executor', fn () => new Executor());
App::setResource('resourceToken', function ($project, $dbForProject, $request) {
App::setResource('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) {
$tokenJWT = $request->getParam('token');
if (!empty($tokenJWT) && !$project->isEmpty()) { // JWT authentication
@@ -1092,7 +1370,7 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
return new Document([]);
}
$token = Authorization::skip(fn () => $dbForProject->getDocument('resourceTokens', $tokenId));
$token = $authorization->skip(fn () => $dbForProject->getDocument('resourceTokens', $tokenId));
if ($token->isEmpty()) {
return new Document([]);
@@ -1110,7 +1388,7 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
}
return match ($token->getAttribute('resourceType')) {
TOKENS_RESOURCE_TYPE_FILES => (function () use ($token, $dbForProject) {
TOKENS_RESOURCE_TYPE_FILES => (function () use ($token, $dbForProject, $authorization) {
$sequences = explode(':', $token->getAttribute('resourceInternalId'));
$ids = explode(':', $token->getAttribute('resourceId'));
@@ -1121,7 +1399,7 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
$accessedAt = $token->getAttribute('accessedAt', 0);
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) {
$token->setAttribute('accessedAt', DatabaseDateTime::now());
Authorization::skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), $token));
$authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), $token));
}
return new Document([
@@ -1136,8 +1414,16 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
};
}
return new Document([]);
}, ['project', 'dbForProject', 'request']);
}, ['project', 'dbForProject', 'request', 'authorization']);
App::setResource('transactionState', function (Database $dbForProject) {
return new TransactionState($dbForProject);
}, ['dbForProject']);
App::setResource('transactionState', function (Database $dbForProject, Authorization $authorization) {
return new TransactionState($dbForProject, $authorization);
}, ['dbForProject', 'authorization']);
App::setResource('executionsRetentionCount', function (Document $project, array $plan) {
if ($project->getId() === 'console' || empty($plan)) {
return 0;
}
return (int) ($plan['executionsRetentionCount'] ?? 100);
}, ['project', 'plan']);
+44 -14
View File
@@ -29,10 +29,10 @@ use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
@@ -67,6 +67,7 @@ if (!function_exists('getConsoleDB')) {
$adapter = new DatabasePool($pools->get('console'));
$database = new Database($adapter, getCache());
$database
->setDatabase(APP_DATABASE)
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', '_console');
@@ -123,6 +124,7 @@ if (!function_exists('getProjectDB')) {
}
$database
->setDatabase(APP_DATABASE)
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId());
@@ -309,7 +311,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
'value' => '{}'
]);
$statsDocument = Authorization::skip(fn () => $database->createDocument('realtime', $document));
$statsDocument = $database->getAuthorization()->skip(fn () => $database->createDocument('realtime', $document));
break;
} catch (Throwable) {
Console::warning("Collection not ready. Retrying connection ({$attempts})...");
@@ -339,7 +341,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
->setAttribute('timestamp', DateTime::now())
->setAttribute('value', json_encode($payload));
Authorization::skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument));
$database->getAuthorization()->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument));
} catch (Throwable $th) {
$logError($th, "updateWorkerDocument");
}
@@ -370,7 +372,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$payload = [];
$list = Authorization::skip(fn () => $database->find('realtime', [
$list = $database->getAuthorization()->skip(fn () => $database->find('realtime', [
Query::greaterThan('timestamp', DateTime::addSeconds(new \DateTime(), -15)),
]));
@@ -464,17 +466,18 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
if ($realtime->hasSubscriber($projectId, 'user:' . $userId)) {
$connection = array_key_first(reset($realtime->subscriptions[$projectId]['user:' . $userId]));
$consoleDatabase = getConsoleDB();
$project = Authorization::skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
$project = $consoleDatabase->getAuthorization()->skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
$database = getProjectDB($project);
/** @var Appwrite\Utopia\Database\Documents\User $user */
$user = $database->getDocument('users', $userId);
$roles = $user->getRoles();
$roles = $user->getRoles($database->getAuthorization());
$channels = $realtime->connections[$connection]['channels'];
$queries = $realtime->connections[$connection]['queries'] ?? [];
$realtime->unsubscribe($connection);
$realtime->subscribe($projectId, $connection, $roles, $channels);
$realtime->subscribe($projectId, $connection, $roles, $channels, $queries);
}
}
@@ -526,6 +529,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
try {
/** @var Document $project */
$project = $app->getResource('project');
$authorization = $app->getResource('authorization');
/*
* Project Check
@@ -537,7 +541,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
if (
array_key_exists('realtime', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['realtime']
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
@@ -573,9 +577,14 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
}
$roles = $user->getRoles();
$roles = $user->getRoles($authorization);
$channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId());
try {
$queries = Realtime::convertQueries($request->getQuery('queries', []));
} catch (QueryException $e) {
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $e->getMessage());
}
/**
* Channels Check
@@ -584,7 +593,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing channels');
}
$realtime->subscribe($project->getId(), $connection, $roles, $channels);
$realtime->subscribe($project->getId(), $connection, $roles, $channels, $queries);
$realtime->connections[$connection]['authorization'] = $authorization;
$user = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT);
@@ -592,6 +603,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
'type' => 'connected',
'data' => [
'channels' => array_keys($channels),
'queries' => $queries,
'user' => $user
]
]));
@@ -614,6 +626,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$code = 500;
}
$message = $th->getMessage();
// sanitize 0 && 5xx errors
@@ -643,12 +656,19 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) {
try {
$response = new Response(new SwooleResponse());
$projectId = $realtime->connections[$connection]['projectId'];
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
// Get authorization from connection (stored during onOpen)
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
$database = getConsoleDB();
$database->setAuthorization($authorization);
if ($projectId !== 'console') {
$project = Authorization::skip(fn () => $database->getDocument('projects', $projectId));
$project = $authorization->skip(fn () => $database->getDocument('projects', $projectId));
$database = getProjectDB($project);
$database->setAuthorization($authorization);
} else {
$project = null;
}
@@ -712,9 +732,19 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Session is not valid.');
}
$roles = $user->getRoles();
$roles = $user->getRoles($database->getAuthorization());
$channels = Realtime::convertChannels(array_flip($realtime->connections[$connection]['channels']), $user->getId());
$realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels);
// Preserve authorization before subscribe overwrites the connection array
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
$queries = $realtime->connections[$connection]['queries'];
$realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels, $queries);
// Restore authorization after subscribe
if ($authorization !== null) {
$realtime->connections[$connection]['authorization'] = $authorization;
}
$user = $response->output($user, Response::MODEL_ACCOUNT);
$server->send([$connection], json_encode([
+1
View File
@@ -165,6 +165,7 @@ $enableAssistant = $this->getParam('enableAssistant', false);
- _APP_MAINTENANCE_RETENTION_SCHEDULES
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
- _APP_GRAPHQL_INTROSPECTION
- _APP_GRAPHQL_MAX_BATCH_SIZE
- _APP_GRAPHQL_MAX_COMPLEXITY
- _APP_GRAPHQL_MAX_DEPTH
+52 -27
View File
@@ -14,6 +14,7 @@ use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Platform\Appwrite;
@@ -48,19 +49,31 @@ use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Telemetry\Adapter\None as NoTelemetry;
Authorization::disable();
Runtime::enableCoroutine();
Server::setResource('register', fn () => $register);
Server::setResource('dbForPlatform', function (Cache $cache, Registry $register) {
Server::setResource('authorization', function () {
$authorization = new Authorization();
$authorization->disable();
return $authorization;
}, []);
Server::setResource('dbForPlatform', function (Cache $cache, Registry $register, Authorization $authorization) {
$pools = $register->get('pools');
$adapter = new DatabasePool($pools->get('console'));
$dbForPlatform = new Database($adapter, $cache);
$dbForPlatform->setNamespace('_console');
$dbForPlatform->setDocumentType('users', User::class);
$dbForPlatform
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setNamespace('_console')
->setDocumentType('users', User::class)
;
return $dbForPlatform;
}, ['cache', 'register']);
}, ['cache', 'register', 'authorization']);
Server::setResource('project', function (Message $message, Database $dbForPlatform) {
$payload = $message->getPayload() ?? [];
@@ -73,7 +86,7 @@ Server::setResource('project', function (Message $message, Database $dbForPlatfo
return $dbForPlatform->getDocument('projects', $project->getId());
}, ['message', 'dbForPlatform']);
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForPlatform) {
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForPlatform, Authorization $authorization) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -105,15 +118,18 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
->setNamespace('_' . $project->getSequence());
}
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
}, ['cache', 'register', 'message', 'project', 'dbForPlatform']);
}, ['cache', 'register', 'message', 'project', 'dbForPlatform', 'authorization']);
Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases): Database {
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -127,7 +143,7 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
if (isset($databases[$dsn->getHost()])) {
$database = $databases[$dsn->getHost()];
$database->setAuthorization($authorization);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
@@ -164,15 +180,18 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
->setNamespace('_' . $project->getSequence());
}
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
};
}, ['pools', 'dbForPlatform', 'cache']);
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
Server::setResource('getLogsDB', function (Group $pools, Cache $cache) {
Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, $database) {
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant((int)$project->getSequence());
return $database;
@@ -182,6 +201,8 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache) {
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
@@ -194,7 +215,7 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache) {
return $database;
};
}, ['pools', 'cache']);
}, ['pools', 'cache', 'authorization']);
Server::setResource('abuseRetention', function () {
return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day
@@ -307,6 +328,10 @@ Server::setResource('queueForBuilds', function (Publisher $publisher) {
return new Build($publisher);
}, ['publisher']);
Server::setResource('queueForScreenshots', function (Publisher $publisher) {
return new Screenshot($publisher);
}, ['publisher']);
Server::setResource('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
@@ -382,10 +407,6 @@ Server::setResource('plan', function (array $plan = []) {
return [];
});
Server::setResource('registryPayments', function (Registry $register) {
return $register->get('registryPayments');
}, ['register']);
Server::setResource('certificates', function () {
$email = System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'));
if (empty($email)) {
@@ -469,6 +490,14 @@ Server::setResource('getAudit', function (Database $dbForPlatform, callable $get
};
}, ['dbForPlatform', 'getProjectDB']);
Server::setResource('executionsRetentionCount', function (Document $project, array $plan) {
if ($project->getId() === 'console' || empty($plan)) {
return 0;
}
return (int) ($plan['executionsRetentionCount'] ?? 100);
}, ['project', 'plan']);
$pools = $register->get('pools');
$platform = new Appwrite();
$args = $platform->getEnv('argv');
@@ -513,7 +542,8 @@ $worker
->inject('log')
->inject('pools')
->inject('project')
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project) use ($worker, $queueName) {
->inject('authorization')
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project, Authorization $authorization) use ($worker, $queueName) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
if ($logger) {
@@ -529,7 +559,7 @@ $worker
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
$log->addExtra('roles', Authorization::getRoles());
$log->addExtra('roles', $authorization->getRoles());
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
@@ -548,9 +578,4 @@ $worker
Console::error('[Error] Line: ' . $error->getLine());
});
$worker->workerStart()
->action(function () use ($workerName) {
Console::info("Worker $workerName started");
});
$worker->start();
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
php /usr/src/code/app/cli.php schedule-payments-usage $@
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
php /usr/src/code/app/worker.php payments-usage-sync $@
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
exec php /usr/src/code/app/worker.php screenshots "$@"
+10 -10
View File
@@ -45,18 +45,18 @@
"ext-sockets": "*",
"appwrite/php-runtimes": "0.19.*",
"appwrite/php-clamav": "2.0.*",
"utopia-php/abuse": "1.*.*",
"utopia-php/abuse": "1.*",
"utopia-php/analytics": "0.10.*",
"utopia-php/audit": "2.0.2-rc3",
"utopia-php/audit": "2.*",
"utopia-php/auth": "0.5.*",
"utopia-php/cache": "0.13.*",
"utopia-php/cli": "0.15.*",
"utopia-php/config": "1.*.*",
"utopia-php/database": "3.*.*",
"utopia-php/config": "1.*",
"utopia-php/database": "4.*",
"utopia-php/detector": "0.2.*",
"utopia-php/domains": "0.9.*",
"utopia-php/domains": "0.11.*",
"utopia-php/emails": "0.6.*",
"utopia-php/dns": "1.4.*",
"utopia-php/dns": "1.5.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/framework": "0.33.*",
"utopia-php/fetch": "0.5.*",
@@ -64,15 +64,15 @@
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.20.*",
"utopia-php/migration": "1.*.*",
"utopia-php/migration": "1.*",
"utopia-php/orchestration": "0.9.*",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "0.8.*",
"utopia-php/preloader": "0.2.*",
"utopia-php/queue": "0.11.*",
"utopia-php/queue": "0.15.*",
"utopia-php/registry": "0.5.*",
"utopia-php/storage": "0.18.*",
"utopia-php/swoole": "0.8.*",
"utopia-php/swoole": "1.*",
"utopia-php/system": "0.9.*",
"utopia-php/telemetry": "0.1.*",
"utopia-php/vcs": "0.13.*",
@@ -109,4 +109,4 @@
"tbachert/spi": true
}
}
}
}
Generated
+9064 -8148
View File
File diff suppressed because it is too large Load Diff
+75 -64
View File
@@ -98,6 +98,7 @@ services:
- ./public:/usr/src/code/public
- ./src:/usr/src/code/src
- ./dev:/usr/src/code/dev
# - ./vendor/utopia-php/framework:/usr/src/code/vendor/utopia-php/framework
depends_on:
- mariadb
- redis
@@ -112,6 +113,7 @@ services:
- _APP_EDITION
- _APP_WORKER_PER_CORE
- _APP_LOCALE
- _APP_COMPRESSION_ENABLED
- _APP_COMPRESSION_MIN_SIZE_BYTES
- _APP_CONSOLE_WHITELIST_ROOT
- _APP_CONSOLE_WHITELIST_EMAILS
@@ -200,6 +202,7 @@ services:
- _APP_MAINTENANCE_RETENTION_SCHEDULES
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
- _APP_GRAPHQL_INTROSPECTION
- _APP_GRAPHQL_MAX_BATCH_SIZE
- _APP_GRAPHQL_MAX_COMPLEXITY
- _APP_GRAPHQL_MAX_DEPTH
@@ -466,14 +469,12 @@ services:
- appwrite-functions:/storage/functions:rw
- appwrite-sites:/storage/sites:rw
- appwrite-builds:/storage/builds:rw
- appwrite-uploads:/storage/uploads:rw
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
environment:
- _APP_BROWSER_HOST
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
@@ -529,6 +530,65 @@ services:
extra_hosts:
- "host.docker.internal:host-gateway"
appwrite-worker-screenshots:
entrypoint: worker-screenshots
<<: *x-logging
container_name: appwrite-worker-screenshots
image: appwrite-dev
networks:
- appwrite
volumes:
- appwrite-uploads:/storage/uploads:rw
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
environment:
# Specific
- _APP_BROWSER_HOST
# Basic
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_LOGGING_CONFIG
# Database
- _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DATABASE_SHARED_TABLES
# Storage
- _APP_STORAGE_DEVICE
- _APP_STORAGE_S3_ACCESS_KEY
- _APP_STORAGE_S3_SECRET
- _APP_STORAGE_S3_REGION
- _APP_STORAGE_S3_BUCKET
- _APP_STORAGE_S3_ENDPOINT
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
- _APP_STORAGE_DO_SPACES_SECRET
- _APP_STORAGE_DO_SPACES_REGION
- _APP_STORAGE_DO_SPACES_BUCKET
- _APP_STORAGE_BACKBLAZE_ACCESS_KEY
- _APP_STORAGE_BACKBLAZE_SECRET
- _APP_STORAGE_BACKBLAZE_REGION
- _APP_STORAGE_BACKBLAZE_BUCKET
- _APP_STORAGE_LINODE_ACCESS_KEY
- _APP_STORAGE_LINODE_SECRET
- _APP_STORAGE_LINODE_REGION
- _APP_STORAGE_LINODE_BUCKET
- _APP_STORAGE_WASABI_ACCESS_KEY
- _APP_STORAGE_WASABI_SECRET
- _APP_STORAGE_WASABI_REGION
- _APP_STORAGE_WASABI_BUCKET
extra_hosts:
- "host.docker.internal:host-gateway"
appwrite-worker-certificates:
entrypoint: worker-certificates
<<: *x-logging
@@ -580,9 +640,12 @@ services:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
- openruntimes-executor
redis:
condition: service_started
mariadb:
condition: service_started
openruntimes-executor:
condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -823,6 +886,7 @@ services:
- _APP_DB_PASS
- _APP_DATABASE_SHARED_TABLES
- _APP_INTERVAL_DOMAIN_VERIFICATION
- _APP_INTERVAL_CLEANUP_STALE_EXECUTIONS
appwrite-task-stats-resources:
container_name: appwrite-task-stats-resources
@@ -917,65 +981,6 @@ services:
- _APP_USAGE_AGGREGATION_INTERVAL
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-payments-usage:
entrypoint: worker-payments-usage
<<: *x-logging
container_name: appwrite-worker-payments-usage
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_LOGGING_CONFIG
- _APP_DATABASE_SHARED_TABLES
appwrite-task-scheduler-payments-usage:
entrypoint: schedule-payments-usage
<<: *x-logging
container_name: appwrite-task-scheduler-payments-usage
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- mariadb
- redis
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_CONFIG
- _APP_DATABASE_SHARED_TABLES
- _APP_PAYMENTS_USAGE_SYNC_INTERVAL
appwrite-task-scheduler-functions:
entrypoint: schedule-functions
<<: *x-logging
@@ -1125,6 +1130,12 @@ services:
- OPR_EXECUTOR_STORAGE_WASABI_SECRET=$_APP_STORAGE_WASABI_SECRET
- OPR_EXECUTOR_STORAGE_WASABI_REGION=$_APP_STORAGE_WASABI_REGION
- OPR_EXECUTOR_STORAGE_WASABI_BUCKET=$_APP_STORAGE_WASABI_BUCKET
healthcheck:
test: ["CMD-SHELL", "curl -fsS -H \"Authorization: Bearer $$OPR_EXECUTOR_SECRET\" http://localhost/v1/health >/dev/null"]
interval: 5s
timeout: 3s
retries: 20
start_period: 5s
mariadb:
image: mariadb:10.11 # fix issues when upgrading using: mysql_upgrade -u root -p
@@ -0,0 +1 @@
appwrite health get-queue-audits
@@ -0,0 +1,3 @@
appwrite projects update-labels \
--project-id <PROJECT_ID> \
--labels one two three
@@ -0,0 +1 @@
Get the number of audit logs that are waiting to be processed in the Appwrite internal queue server.
+58
View File
@@ -1,5 +1,63 @@
# Change Log
## 13.1.0
- Mark `appwrite generate` command as stable
- Improve permissions param to be a typesafe callback
- Fix relationship handling in generated code
- Fix `appwrite client` properly hanlding `--key` parameter
- Fix `init site` not working on Windows
## 13.1.0-rc.3
- Allow generation of server side CRUD operations on databases and tables
- Fix npm distribution failing due to missing template files in bundle
## 13.1.0-rc.2
- Update generated `databases` services to automatically initialize a client instance
- Update generator to use handlebars templates
## 13.1.0-rc.1
- Feat: `appwrite generate` command to create a fully typesafe SDK for your Appwrite project
- Chore: improve creation of columns during table creation by passing them directly instead of creating them one by one
- Improved config validation by adding extra rules in zod schema
## 13.0.1
- Fix `project init` command leading to Cannot convert to BigInt error
- Fix filter out unwanted attributes being pulled in the config file
## 13.0.0
- Mark release as stable
- Feat: add pull sync on destruction of remote resources (+ confirmation)
- Fix: refine zod schema to check string size
- Validate using zod schema during push cli command
- Maintain order of keys in local config
## 13.0.0-rc.5
- Fix push all command not working correctly
## 13.0.0-rc.4
- Fix CLI ES module import issues
## 13.0.0-rc.3
- Add `Schema` class for programmatically pushing and pulling appwrite config
- Add client side db generation using `schema.db.generate()` command
## 13.0.0-rc.2
- Fixes a lot of typescript errors throughout the codebase
## 13.0.0-rc.1
- Migrates codebase from JavaScript to TypeScript
## 12.0.1
Fix type generation for `point`, `lineString` and `polygon` columns
+4
View File
@@ -1,5 +1,9 @@
# Change Log
## 20.1.1
* Fix boolean parameter not handled correctly in Client requests
## 20.1.0
* Added ability to create columns and indexes synchronously while creating a table
+4
View File
@@ -1,5 +1,9 @@
# Change Log
## 20.3.3
* Fix boolean parameter not handled correctly in Client requests
## 20.3.2
* Fix OAuth2 browser infinite redirect issue
+5
View File
@@ -0,0 +1,5 @@
# Change Log
## 0.1.0
* Initial release
+93 -3
View File
@@ -15,6 +15,8 @@ class Key
{
public function __construct(
protected string $projectId,
protected string $teamId,
protected string $userId,
protected string $type,
protected string $role,
protected array $scopes,
@@ -34,6 +36,16 @@ class Key
return $this->projectId;
}
public function getUserId(): string
{
return $this->userId;
}
public function getTeamId(): string
{
return $this->teamId;
}
public function getType(): string
{
return $this->type;
@@ -95,13 +107,12 @@ class Key
* Decode the given secret key into a Key object, containing the project ID, type, role, scopes, and name.
* Can be a stored API key or a dynamic key (JWT).
*
* @param Document $project
* @param string $key
* @return Key
* @throws Exception
*/
public static function decode(
Document $project,
Document $team,
Document $user,
string $key
): Key {
if (\str_contains($key, '_')) {
@@ -118,6 +129,8 @@ class Key
$guestKey = new Key(
$project->getId(),
'',
'',
$type,
User::ROLE_GUESTS,
$roles[User::ROLE_GUESTS]['scopes'] ?? [],
@@ -133,6 +146,7 @@ class Key
leeway: 0
);
$payload = [];
try {
$payload = $jwtObj->decode($secret);
} catch (JWTException) {
@@ -155,6 +169,8 @@ class Key
return new Key(
$projectId,
'',
'',
$type,
$role,
$scopes,
@@ -188,12 +204,86 @@ class Key
return new Key(
$project->getId(),
'',
'',
$type,
$role,
$scopes,
$name,
$expired
);
case API_KEY_ACCOUNT:
$key = $user->find(
key: 'secret',
find: $key,
subject: 'keys'
);
// Invalid key
if (!$key) {
return $guestKey;
}
$expire = $key->getAttribute('expire');
$expired = false;
if (!empty($expire) && $expire < DateTime::formatTz(DateTime::now())) {
$expired = true;
}
$name = $key->getAttribute('name', 'UNKNOWN');
$role = User::ROLE_USERS;
$scopes = $key->getAttribute('scopes', []);
$key = new Key(
'',
'',
$user->getId(),
$type,
$role,
$scopes,
$name,
$expired
);
return $key;
case API_KEY_ORGANIZATION:
$key = $team->find(
key: 'secret',
find: $key,
subject: 'keys'
);
// Invalid key
if (!$key) {
return $guestKey;
}
$expire = $key->getAttribute('expire');
$expired = false;
if (!empty($expire) && $expire < DateTime::formatTz(DateTime::now())) {
$expired = true;
}
$name = $key->getAttribute('name', 'UNKNOWN');
$role = User::ROLE_APPS;
$scopes = $key->getAttribute('scopes', []);
$key = new Key(
'',
$team->getId(),
'',
$type,
$role,
$scopes,
$name,
$expired
);
return $key;
default:
return $guestKey;
}
+6 -4
View File
@@ -20,10 +20,12 @@ use Utopia\Database\Validator\Authorization;
class TransactionState
{
private Database $dbForProject;
public function __construct(Database $dbForProject)
private Authorization $authorization;
/** @var Authorization $authorization */
public function __construct(Database $dbForProject, Authorization $authorization)
{
$this->dbForProject = $dbForProject;
$this->authorization = $authorization;
}
@@ -342,12 +344,12 @@ class TransactionState
*/
private function getTransactionState(string $transactionId): array
{
$transaction = Authorization::skip(fn () => $this->dbForProject->getDocument('transactions', $transactionId));
$transaction = $this->authorization->skip(fn () => $this->dbForProject->getDocument('transactions', $transactionId));
if ($transaction->isEmpty() || $transaction->getAttribute('status') !== 'pending') {
return [];
}
$operations = Authorization::skip(fn () => $this->dbForProject->find('transactionLogs', [
$operations = $this->authorization->skip(fn () => $this->dbForProject->find('transactionLogs', [
Query::equal('transactionInternalId', [$transaction->getSequence()]),
Query::orderAsc(),
Query::limit(PHP_INT_MAX)
+13 -6
View File
@@ -3,8 +3,10 @@
namespace Appwrite\Deletes;
use Appwrite\Extend\Exception;
use Utopia\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Query;
class Targets
@@ -42,12 +44,17 @@ class Targets
MESSAGE_TYPE_PUSH => 'pushTotal',
default => throw new Exception('Invalid target provider type'),
};
$database->decreaseDocumentAttribute(
'topics',
$topicId,
$totalAttribute,
min: 0
);
try {
$database->decreaseDocumentAttribute(
'topics',
$topicId,
$totalAttribute,
min: 0
);
} catch (LimitException $e) {
Console::error("Delete subscribers decreaseDocumentAttribute (topicId={$topicId}): {$e->getMessage()}");
}
}
}
);
+3 -3
View File
@@ -39,15 +39,15 @@ class Event
public const BUILDS_QUEUE_NAME = 'v1-builds';
public const BUILDS_CLASS_NAME = 'BuildsV1';
public const SCREENSHOTS_QUEUE_NAME = 'v1-screenshots';
public const SCREENSHOTS_CLASS_NAME = 'ScreenshotsV1';
public const MESSAGING_QUEUE_NAME = 'v1-messaging';
public const MESSAGING_CLASS_NAME = 'MessagingV1';
public const MIGRATIONS_QUEUE_NAME = 'v1-migrations';
public const MIGRATIONS_CLASS_NAME = 'MigrationsV1';
public const PAYMENTS_USAGE_QUEUE_NAME = 'v1-payments-usage-sync';
public const PAYMENTS_USAGE_CLASS_NAME = 'PaymentsUsageSyncV1';
protected string $queue = '';
protected string $class = '';
protected string $event = '';
-29
View File
@@ -1,29 +0,0 @@
<?php
namespace Appwrite\Event;
use Utopia\Queue\Publisher;
class PaymentsUsage extends Event
{
public function __construct(protected Publisher $publisher)
{
parent::__construct($publisher);
$this
->setQueue(Event::PAYMENTS_USAGE_QUEUE_NAME)
->setClass(Event::PAYMENTS_USAGE_CLASS_NAME);
}
/**
* Prepare the payload for the event
*
* @return array
*/
protected function preparePayload(): array
{
return [
'project' => $this->getProject(),
];
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
namespace Appwrite\Event;
use Utopia\Config\Config;
use Utopia\Queue\Publisher;
use Utopia\System\System;
class Screenshot extends Event
{
protected string $deploymentId = '';
public function __construct(protected Publisher $publisher)
{
parent::__construct($publisher);
$this
->setQueue(System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME))
->setClass(System::getEnv('_APP_SCREENSHOTS_CLASS_NAME', Event::SCREENSHOTS_CLASS_NAME));
}
public function setDeploymentId(string $deploymentId): self
{
$this->deploymentId = $deploymentId;
return $this;
}
protected function preparePayload(): array
{
$platform = $this->platform;
if (empty($platform)) {
$platform = Config::getParam('platform', []);
}
return [
'project' => $this->project,
'deploymentId' => $this->deploymentId,
'platform' => $platform,
];
}
public function reset(): self
{
$this->deploymentId = '';
parent::reset();
return $this;
}
}
+7 -12
View File
@@ -62,7 +62,6 @@ class Exception extends \Exception
public const string GENERAL_INVALID_PHONE = 'general_invalid_phone';
public const string GENERAL_REGION_ACCESS_DENIED = 'general_region_access_denied';
public const string GENERAL_BAD_REQUEST = 'general_bad_request';
public const string GENERAL_NOT_FOUND = 'general_not_found';
/** Users */
public const string USER_COUNT_EXCEEDED = 'user_count_exceeded';
@@ -108,7 +107,9 @@ class Exception extends \Exception
public const string USER_DELETION_PROHIBITED = 'user_deletion_prohibited';
public const string USER_TARGET_NOT_FOUND = 'user_target_not_found';
public const string USER_TARGET_ALREADY_EXISTS = 'user_target_already_exists';
public const string USER_API_KEY_AND_SESSION_SET = 'user_key_and_session_set';
public const string USER_API_KEY_AND_SESSION_SET = 'user_api_key_and_session_set';
public const string USER_JWT_AND_COOKIE_SET = 'user_jwt_and_cookie_set';
public const string USER_ID_MISSING = 'user_id_missing';
public const string API_KEY_EXPIRED = 'api_key_expired';
@@ -120,6 +121,8 @@ class Exception extends \Exception
public const string TEAM_INVITE_MISMATCH = 'team_invite_mismatch';
public const string TEAM_ALREADY_EXISTS = 'team_already_exists';
public const string ORGANIZATION_ID_MISSING = 'organization_id_missing';
/** Console */
public const string RESOURCE_ALREADY_EXISTS = 'resource_already_exists';
@@ -284,6 +287,7 @@ class Exception extends \Exception
/** Projects */
public const string PROJECT_NOT_FOUND = 'project_not_found';
public const string PROJECT_ID_MISSING = 'project_id_missing';
public const string PROJECT_PROVIDER_DISABLED = 'project_provider_disabled';
public const string PROJECT_PROVIDER_UNSUPPORTED = 'project_provider_unsupported';
public const string PROJECT_ALREADY_EXISTS = 'project_already_exists';
@@ -291,6 +295,7 @@ class Exception extends \Exception
public const string PROJECT_INVALID_FAILURE_URL = 'project_invalid_failure_url';
public const string PROJECT_RESERVED_PROJECT = 'project_reserved_project';
public const string PROJECT_KEY_EXPIRED = 'project_key_expired';
public const string ACCOUNT_KEY_EXPIRED = 'account_key_expired';
public const string PROJECT_SMTP_CONFIG_INVALID = 'project_smtp_config_invalid';
@@ -379,16 +384,6 @@ class Exception extends \Exception
public const string TOKEN_EXPIRED = 'token_expired';
public const string TOKEN_RESOURCE_TYPE_INVALID = 'token_resource_type_invalid';
/** Payments */
public const string PAYMENT_PLAN_NOT_FOUND = 'payment_plan_not_found';
public const string PAYMENT_PLAN_ALREADY_EXISTS = 'payment_plan_already_exists';
public const string PAYMENT_SUBSCRIPTION_NOT_FOUND = 'payment_subscription_not_found';
public const string PAYMENT_SUBSCRIPTION_ALREADY_EXISTS = 'payment_subscription_already_exists';
public const string PAYMENT_PROVIDER_NOT_CONFIGURED = 'payment_provider_not_configured';
public const string PAYMENT_PROVIDER_ALREADY_CONFIGURED = 'payment_provider_already_configured';
public const string PAYMENT_WEBHOOK_FAILED = 'payment_webhook_failed';
public const string PAYMENT_FEATURE_NOT_FOUND = 'payment_feature_not_found';
protected string $type = '';
protected array $errors = [];
protected bool $publish;
+106
View File
@@ -0,0 +1,106 @@
<?php
namespace Appwrite\Functions;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
class EventProcessor
{
/**
* Get function events for a project, using Redis cache
* @param Document|null $project
* @param Database $dbForProject
* @return array<string, bool>
*/
public function getFunctionsEvents(?Document $project, Database $dbForProject): array
{
if ($project === null ||
$project->isEmpty() ||
$project->getId() === 'console') {
return [];
}
$hostname = $dbForProject->getAdapter()->getHostname();
$cacheKey = \sprintf(
'%s-cache-%s:%s:%s:project:%s:functions:events',
$dbForProject->getCacheName(),
$hostname ?? '',
$dbForProject->getNamespace(),
$dbForProject->getTenant(),
$project->getId()
);
$ttl = 3600; // 1 hour cache TTL
$cachedFunctionEvents = $dbForProject->getCache()->load($cacheKey, $ttl);
if ($cachedFunctionEvents !== false) {
return \json_decode($cachedFunctionEvents, true) ?? [];
}
try {
$events = [];
$limit = 100;
$sum = 100;
$offset = 0;
while ($sum >= $limit) {
$functions = $dbForProject->find('functions', [
Query::select(['$id', 'events']),
Query::limit($limit),
Query::offset($offset),
Query::orderAsc('$sequence'),
]);
$sum = \count($functions);
$offset = $offset + $limit;
foreach ($functions as $function) {
$functionEvents = $function->getAttribute('events', []);
if (!empty($functionEvents)) {
$events = array_merge($events, $functionEvents);
}
}
}
$uniqueEvents = \array_flip(\array_unique($events));
$dbForProject->getCache()->save($cacheKey, \json_encode($uniqueEvents));
return $uniqueEvents;
} catch (\Throwable $e) {
return [];
}
}
/**
* Get webhook events for a project from the project's webhooks attribute
* @param Document|null $project
* @return array<string, bool>
*/
public function getWebhooksEvents(?Document $project): array
{
if ($project === null || $project->isEmpty() || $project->getId() === 'console') {
return [];
}
$webhooks = $project->getAttribute('webhooks', []);
if (empty($webhooks)) {
return [];
}
$events = [];
foreach ($webhooks as $webhook) {
if ($webhook->getAttribute('enabled', false) !== true) {
continue;
}
$webhookEvents = $webhook->getAttribute('events', []);
if (!empty($webhookEvents)) {
$events = array_merge($events, $webhookEvents);
}
}
return \array_flip(\array_unique($events));
}
}
+43 -3
View File
@@ -4,10 +4,13 @@ namespace Appwrite\Messaging\Adapter;
use Appwrite\Messaging\Adapter as MessagingAdapter;
use Appwrite\PubSub\Adapter\Pool as PubSubPool;
use Appwrite\Utopia\Database\RuntimeQuery;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
class Realtime extends MessagingAdapter
{
@@ -51,9 +54,10 @@ class Realtime extends MessagingAdapter
* @param mixed $identifier
* @param array $roles
* @param array $channels
* @param array $queries
* @return void
*/
public function subscribe(string $projectId, mixed $identifier, array $roles, array $channels): void
public function subscribe(string $projectId, mixed $identifier, array $roles, array $channels, array $queries = []): void
{
if (!isset($this->subscriptions[$projectId])) { // Init Project
$this->subscriptions[$projectId] = [];
@@ -72,7 +76,8 @@ class Realtime extends MessagingAdapter
$this->connections[$identifier] = [
'projectId' => $projectId,
'roles' => $roles,
'channels' => $channels
'channels' => $channels,
'queries' => $queries
];
}
@@ -206,7 +211,14 @@ class Realtime extends MessagingAdapter
/**
* To prevent duplicates, we save the connections as array keys.
*/
$receivers[$id] = 0;
$queries = $this->connections[$id]['queries'] ?? [];
$payload = $event['data']['payload'] ?? [];
if (
empty($queries) ||
!empty(RuntimeQuery::filter($queries, $payload))
) {
$receivers[$id] = 0;
}
}
break;
}
@@ -245,6 +257,34 @@ class Realtime extends MessagingAdapter
return $channels;
}
/**
* Converts the queries from the Query Params into an array.
* @param array $queries
* @return array
*/
public static function convertQueries(array $queries): array
{
$queries = Query::parseQueries($queries);
$stack = $queries;
$allowedMethods = implode(', ', RuntimeQuery::ALLOWED_QUERIES);
while (!empty($stack)) {
/** `@var` Query $query */
$query = array_pop($stack);
$method = $query->getMethod();
if (!in_array($method, RuntimeQuery::ALLOWED_QUERIES, true)) {
$unsupportedMethod = $method;
throw new QueryException(
"Query method '{$unsupportedMethod}' is not supported in Realtime queries. Allowed query methods are: {$allowedMethods}"
);
}
if (in_array($method, [Query::TYPE_AND, Query::TYPE_OR], true)) {
$stack = array_merge($stack, $query->getValues());
}
}
return $queries;
}
/**
* Create channels array based on the event name and payload.
*
+4 -2
View File
@@ -100,8 +100,6 @@ abstract class Migration
public function __construct()
{
Authorization::disable();
Authorization::setDefaultStatus(false);
$this->collections = Config::getParam('collections', []);
@@ -129,6 +127,7 @@ abstract class Migration
Document $project,
Database $dbForProject,
Database $dbForPlatform,
Authorization $authorization,
?callable $getProjectDB = null
): self {
$this->project = $project;
@@ -136,6 +135,9 @@ abstract class Migration
$this->dbForPlatform = $dbForPlatform;
$this->getProjectDB = $getProjectDB;
$authorization->disable();
$authorization->setDefaultStatus(false);
return $this;
}
@@ -1,47 +0,0 @@
<?php
namespace Appwrite\Payments\Provider;
use Utopia\Database\Document;
interface Adapter
{
public function getIdentifier(): string;
public function configure(array $config, Document $project): ProviderState;
public function ensurePlan(array $planData, ProviderState $state): ProviderPlanRef;
public function updatePlan(array $planData, ProviderPlanRef $reference, ProviderState $state): ProviderPlanRef;
public function deletePlan(ProviderPlanRef $reference, ProviderState $state): void;
public function ensureFeature(array $featureData, ProviderPlanRef $plan, ProviderState $state): ProviderFeatureRef;
public function deleteFeature(ProviderFeatureRef $feature, ProviderPlanRef $plan, ProviderState $state): void;
public function updateSubscription(ProviderSubscriptionRef $subscription, array $changes, ProviderState $state): ProviderSubscriptionRef;
public function cancelSubscription(ProviderSubscriptionRef $subscription, bool $atPeriodEnd, ProviderState $state): ProviderSubscriptionRef;
public function resumeSubscription(ProviderSubscriptionRef $subscription, ProviderState $state): ProviderSubscriptionRef;
public function createCheckoutSession(Document $actor, array $planContext, ProviderState $state, array $options = []): ProviderCheckoutSession;
public function createPortalSession(Document $actor, ProviderState $state, array $options = []): ProviderPortalSession;
/**
* @return ProviderInvoice[]
*/
public function listInvoices(ProviderSubscriptionRef $subscription, ProviderState $state, int $limit = 25, int $offset = 0): array;
public function previewProration(ProviderSubscriptionRef $subscription, string $newPriceId, ProviderState $state): ProviderProrationPreview;
public function reportUsage(ProviderSubscriptionRef $subscription, string $featureId, int $quantity, \DateTimeInterface $timestamp, ProviderState $state): void;
public function syncUsage(ProviderSubscriptionRef $subscription, ProviderState $state): ProviderUsageReport;
public function handleWebhook(array $payload, ProviderState $state): ProviderWebhookResult;
public function testConnection(array $config): ProviderTestResult;
}
@@ -1,12 +0,0 @@
<?php
namespace Appwrite\Payments\Provider;
class ProviderCheckoutSession
{
public function __construct(
public readonly string $url,
public readonly array $metadata = [],
) {
}
}
@@ -1,12 +0,0 @@
<?php
namespace Appwrite\Payments\Provider;
class ProviderFeatureRef
{
public function __construct(
public readonly string $externalFeatureId,
public readonly array $metadata = [],
) {
}
}
@@ -1,19 +0,0 @@
<?php
namespace Appwrite\Payments\Provider;
class ProviderInvoice
{
public function __construct(
public readonly string $invoiceId,
public readonly string $subscriptionId,
public readonly int $amount,
public readonly string $currency,
public readonly string $status,
public readonly ?int $createdAt = null,
public readonly ?int $paidAt = null,
public readonly ?string $invoiceUrl = null,
public readonly array $metadata = [],
) {
}
}
@@ -1,12 +0,0 @@
<?php
namespace Appwrite\Payments\Provider;
class ProviderPlanRef
{
public function __construct(
public readonly string $externalPlanId,
public readonly array $metadata = [],
) {
}
}
@@ -1,12 +0,0 @@
<?php
namespace Appwrite\Payments\Provider;
class ProviderPortalSession
{
public function __construct(
public readonly string $url,
public readonly array $metadata = [],
) {
}
}
@@ -1,15 +0,0 @@
<?php
namespace Appwrite\Payments\Provider;
class ProviderProrationPreview
{
public function __construct(
public readonly int $amountDue,
public readonly int $prorationAmount,
public readonly string $currency,
public readonly ?int $nextBillingDate = null,
public readonly array $metadata = [],
) {
}
}
@@ -1,13 +0,0 @@
<?php
namespace Appwrite\Payments\Provider;
class ProviderState
{
public function __construct(
public readonly string $providerId,
public readonly array $config = [],
public readonly array $metadata = [],
) {
}
}
@@ -1,12 +0,0 @@
<?php
namespace Appwrite\Payments\Provider;
class ProviderSubscriptionRef
{
public function __construct(
public readonly string $externalSubscriptionId,
public readonly array $metadata = [],
) {
}
}
@@ -1,13 +0,0 @@
<?php
namespace Appwrite\Payments\Provider;
class ProviderTestResult
{
public function __construct(
public readonly bool $success,
public readonly string $message = '',
public readonly array $metadata = [],
) {
}
}
@@ -1,12 +0,0 @@
<?php
namespace Appwrite\Payments\Provider;
class ProviderUsageReport
{
public function __construct(
public readonly array $totals = [],
public readonly array $details = [],
) {
}
}
@@ -1,12 +0,0 @@
<?php
namespace Appwrite\Payments\Provider;
class ProviderWebhookResult
{
public function __construct(
public readonly string $status,
public readonly array $changes = [],
) {
}
}
@@ -1,45 +0,0 @@
<?php
namespace Appwrite\Payments\Provider;
use Utopia\Database\Document;
class Registry
{
/**
* @var array<string, class-string<Adapter>>
*/
private array $map = [];
/**
* @var array<string, Adapter>
*/
private array $cache = [];
public function __construct()
{
// Map provider identifiers to adapter classes
// e.g., $this->map['stripe'] = \Appwrite\Payments\Provider\StripeAdapter::class;
}
public function register(string $identifier, string $adapterClass): void
{
$this->map[$identifier] = $adapterClass;
}
public function get(string $identifier, array $config, Document $project, \Utopia\Database\Database $dbForPlatform, \Utopia\Database\Database $dbForProject): Adapter
{
$cacheKey = $identifier . ':' . $project->getId();
if (isset($this->cache[$cacheKey])) {
return $this->cache[$cacheKey];
}
if (!isset($this->map[$identifier])) {
throw new \RuntimeException('Unknown payments provider: ' . $identifier);
}
$class = $this->map[$identifier];
/** @var Adapter $adapter */
$adapter = new $class($config, $project, $dbForProject, $dbForPlatform);
$this->cache[$cacheKey] = $adapter;
return $adapter;
}
}
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -3,11 +3,12 @@
namespace Appwrite\Platform;
use Appwrite\Platform\Modules\Account;
use Appwrite\Platform\Modules\Avatars;
use Appwrite\Platform\Modules\Console;
use Appwrite\Platform\Modules\Core;
use Appwrite\Platform\Modules\Databases;
use Appwrite\Platform\Modules\Functions;
use Appwrite\Platform\Modules\Payments;
use Appwrite\Platform\Modules\Health;
use Appwrite\Platform\Modules\Projects;
use Appwrite\Platform\Modules\Proxy;
use Appwrite\Platform\Modules\Sites;
@@ -21,14 +22,15 @@ class Appwrite extends Platform
{
parent::__construct(new Core());
$this->addModule(new Account\Module());
$this->addModule(new Avatars\Module());
$this->addModule(new Databases\Module());
$this->addModule(new Projects\Module());
$this->addModule(new Functions\Module());
$this->addModule(new Health\Module());
$this->addModule(new Sites\Module());
$this->addModule(new Console\Module());
$this->addModule(new Proxy\Module());
$this->addModule(new Tokens\Module());
$this->addModule(new Storage\Module());
$this->addModule(new Payments\Module());
}
}
@@ -17,8 +17,8 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Template\Template;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumberUtil;
use Utopia\Abuse\Abuse;
use Utopia\Auth\Proofs\Code as ProofsCode;
use Utopia\Auth\Proofs\Token as ProofsToken;
use Utopia\Database\Database;
@@ -196,26 +196,21 @@ class Create extends Action
->setRecipients([$phone])
->setProviderType(MESSAGE_TYPE_SMS);
if (isset($plan['authPhone'])) {
$timelimit = $timelimit('organization:{organizationId}', $plan['authPhone'], 30 * 24 * 60 * 60); // 30 days
$timelimit
->setParam('{organizationId}', $project->getAttribute('teamId'));
$helper = PhoneNumberUtil::getInstance();
try {
$countryCode = $helper->parse($phone)->getCountryCode();
$abuse = new Abuse($timelimit);
if ($abuse->check() && System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') {
$helper = PhoneNumberUtil::getInstance();
$countryCode = $helper->parse($phone)->getCountryCode();
if (!empty($countryCode)) {
$queueForStatsUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
if (!empty($countryCode)) {
$queueForStatsUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
$queueForStatsUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
} catch (NumberParseException $e) {
// Ignore invalid phone number for country code stats
}
$queueForStatsUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
break;
case Type::EMAIL:
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
@@ -0,0 +1,158 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action as PlatformAction;
use Appwrite\Utopia\Response;
use Throwable;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Image\Image;
use Utopia\Logger\Logger;
class Action extends PlatformAction
{
protected function getAppRoot(): string
{
return \dirname(__DIR__, 6);
}
protected function avatar(string $type, string $code, int $width, int $height, int $quality, Response $response): void
{
$code = \strtolower($code);
$type = \strtolower($type);
$set = Config::getParam('avatar-' . $type, []);
if (empty($set)) {
throw new Exception(Exception::AVATAR_SET_NOT_FOUND);
}
if (!\array_key_exists($code, $set)) {
throw new Exception(Exception::AVATAR_NOT_FOUND);
}
if (!\extension_loaded('imagick')) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
}
$output = 'png';
$path = $set[$code]['path'];
$type = 'png';
if (!\is_readable($path)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'File not readable in ' . $path);
}
$image = new Image(\file_get_contents($path));
$image->crop((int) $width, (int) $height);
$output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality);
$response
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
->setContentType('image/png')
->file($data);
unset($image);
}
protected function getUserGitHub(string $userId, Document $project, Database $dbForProject, Database $dbForPlatform, ?Logger $logger, Authorization $authorization): array
{
try {
$user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId));
$sessions = $user->getAttribute('sessions', []);
$gitHubSession = null;
foreach ($sessions as $session) {
if ($session->getAttribute('provider', '') === 'github') {
$gitHubSession = $session;
break;
}
}
if (empty($gitHubSession)) {
throw new Exception(Exception::USER_SESSION_NOT_FOUND, 'GitHub session not found.');
}
$provider = $gitHubSession->getAttribute('provider', '');
$accessToken = $gitHubSession->getAttribute('providerAccessToken');
$accessTokenExpiry = $gitHubSession->getAttribute('providerAccessTokenExpiry');
$refreshToken = $gitHubSession->getAttribute('providerRefreshToken');
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
$oAuthProviders = Config::getParam('oAuthProviders');
$className = $oAuthProviders[$provider]['class'];
if (!\class_exists($className)) {
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
$oauth2 = new $className($appId, $appSecret, '', [], []);
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
if ($isExpired) {
try {
$oauth2->refreshTokens($refreshToken);
$accessToken = $oauth2->getAccessToken('');
$refreshToken = $oauth2->getRefreshToken('');
$verificationId = $oauth2->getUserID($accessToken);
if (empty($verificationId)) {
throw new \Exception("Locked tokens."); // Race codition, handeled in catch
}
$gitHubSession
->setAttribute('providerAccessToken', $accessToken)
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry('')));
$authorization->skip(fn () => $dbForProject->updateDocument('sessions', $gitHubSession->getId(), $gitHubSession));
$dbForProject->purgeCachedDocument('users', $user->getId());
} catch (Throwable $err) {
$index = 0;
do {
$previousAccessToken = $gitHubSession->getAttribute('providerAccessToken');
$user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId));
$sessions = $user->getAttribute('sessions', []);
$gitHubSession = new Document();
foreach ($sessions as $session) {
if ($session->getAttribute('provider', '') === 'github') {
$gitHubSession = $session;
break;
}
}
$accessToken = $gitHubSession->getAttribute('providerAccessToken');
if ($accessToken !== $previousAccessToken) {
break;
}
$index++;
\usleep(500000);
} while ($index < 10);
}
}
$oauth2 = new $className($appId, $appSecret, '', [], []);
$githubUser = $oauth2->getUserSlug($accessToken);
$githubId = $oauth2->getUserID($accessToken);
return [
'name' => $githubUser,
'id' => $githubId
];
} catch (Exception $error) {
return [];
}
}
}
@@ -0,0 +1,64 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http\Browsers;
use Appwrite\Platform\Modules\Avatars\Http\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getBrowser';
}
public function __construct()
{
$this
->setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/avatars/browsers/:code')
->desc('Get browser icon')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/browser')
->label('sdk', new Method(
namespace: 'avatars',
group: null,
name: 'getBrowser',
description: '/docs/references/avatars/get-browser.md',
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
type: MethodType::LOCATION,
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::IMAGE_PNG
))
->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-browsers'))), 'Browser Code.')
->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true)
->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true)
->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true)
->inject('response')
->callback($this->action(...));
}
public function action(string $code, int $width, int $height, int $quality, Response $response)
{
$this->avatar('browsers', $code, $width, $height, $quality, $response);
}
}
@@ -0,0 +1,116 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http\Cards\Cloud\Back;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Avatars\Http\Action;
use Appwrite\Utopia\Response;
use Imagick;
use ImagickDraw;
use ImagickPixel;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Logger\Logger;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getCloudCardBack';
}
public function __construct()
{
$this
->setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/cards/cloud-back')
->desc('Get back Of Cloud Card')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resourceType', 'cards/cloud-back')
->label('cache.resource', 'card-back/{request.userId}')
->label('docs', false)
->label('origin', '*')
->param('userId', '', new UID(), 'User ID.', true)
->param('mock', '', new WhiteList(['golden', 'normal', 'platinum']), 'Mocking behaviour.', true)
->param('width', 0, new Range(0, 512), 'Resize image width, Pass an integer between 0 to 512.', true)
->param('height', 0, new Range(0, 320), 'Resize image height, Pass an integer between 0 to 320.', true)
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('response')
->inject('heroes')
->inject('contributors')
->inject('employees')
->inject('logger')
->inject('authorization')
->callback($this->action(...));
}
public function action(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, Authorization $authorization)
{
$user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId));
if ($user->isEmpty() && empty($mock)) {
throw new Exception(Exception::USER_NOT_FOUND);
}
if (!$mock) {
$userId = $user->getId();
$email = $user->getAttribute('email', '');
$gitHub = $this->getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger, $authorization);
$githubId = $gitHub['id'] ?? '';
$isHero = \array_key_exists($email, $heroes);
$isContributor = \in_array($githubId, $contributors);
$isEmployee = \array_key_exists($email, $employees);
$isGolden = $isEmployee || $isHero || $isContributor;
$isPlatinum = $user->getSequence() % 100 === 0;
} else {
$userId = '63e0bcf3c3eb803ba530';
$isGolden = $mock === 'golden';
$isPlatinum = $mock === 'platinum';
}
$userId = 'UID ' . $userId;
$isPlatinum = $isGolden ? false : $isPlatinum;
$imagePath = $isGolden ? 'back-golden.png' : ($isPlatinum ? 'back-platinum.png' : 'back.png');
$baseImage = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/' . $imagePath);
setlocale(LC_ALL, "en_US.utf8");
// $userId = \iconv("utf-8", "ascii//TRANSLIT", $userId);
$text = new ImagickDraw();
$text->setTextAlignment(Imagick::ALIGN_CENTER);
$text->setFont($this->getAppRoot() . '/public/fonts/SourceCodePro-Regular.ttf');
$text->setFillColor(new ImagickPixel($isGolden ? '#664A1E' : ($isPlatinum ? '#555555' : '#E8E9F0')));
$text->setFontSize(28);
$text->setFontWeight(400);
$baseImage->annotateImage($text, 512, 596, 0, $userId);
if (!empty($width) || !empty($height)) {
$baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
}
$response
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
->setContentType('image/png')
->file($baseImage->getImageBlob());
}
}
@@ -0,0 +1,245 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http\Cards\Cloud\Front;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Avatars\Http\Action;
use Appwrite\Utopia\Response;
use Imagick;
use ImagickDraw;
use ImagickPixel;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Logger\Logger;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getCloudCard';
}
public function __construct()
{
$this
->setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/cards/cloud')
->desc('Get front Of Cloud Card')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resourceType', 'cards/cloud')
->label('cache.resource', 'card/{request.userId}')
->label('docs', false)
->label('origin', '*')
->param('userId', '', new UID(), 'User ID.', true)
->param('mock', '', new WhiteList(['employee', 'employee-2digit', 'hero', 'contributor', 'normal', 'platinum', 'normal-no-github', 'normal-long']), 'Mocking behaviour.', true)
->param('width', 0, new Range(0, 512), 'Resize image width, Pass an integer between 0 to 512.', true)
->param('height', 0, new Range(0, 320), 'Resize image height, Pass an integer between 0 to 320.', true)
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('response')
->inject('heroes')
->inject('contributors')
->inject('employees')
->inject('logger')
->inject('authorization')
->callback($this->action(...));
}
public function action(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, Authorization $authorization)
{
$user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId));
if ($user->isEmpty() && empty($mock)) {
throw new Exception(Exception::USER_NOT_FOUND);
}
if (!$mock) {
$name = $user->getAttribute('name', 'Anonymous');
$email = $user->getAttribute('email', '');
$createdAt = new \DateTime($user->getCreatedAt());
$gitHub = $this->getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger, $authorization);
$githubName = $gitHub['name'] ?? '';
$githubId = $gitHub['id'] ?? '';
$isHero = \array_key_exists($email, $heroes);
$isContributor = \in_array($githubId, $contributors);
$isEmployee = \array_key_exists($email, $employees);
$employeeNumber = $isEmployee ? $employees[$email]['spot'] : '';
if ($isHero) {
$createdAt = new \DateTime($heroes[$email]['memberSince'] ?? '');
} elseif ($isEmployee) {
$createdAt = new \DateTime($employees[$email]['memberSince'] ?? '');
}
if (!$isEmployee && !empty($githubName)) {
$employeeGitHub = \array_search(\strtolower($githubName), \array_map(fn ($employee) => \strtolower($employee['gitHub']) ?? '', $employees));
if (!empty($employeeGitHub)) {
$isEmployee = true;
$employeeNumber = $isEmployee ? $employees[$employeeGitHub]['spot'] : '';
$createdAt = new \DateTime($employees[$employeeGitHub]['memberSince'] ?? '');
}
}
$isPlatinum = $user->getSequence() % 100 === 0;
} else {
$name = $mock === 'normal-long' ? 'Sir First Walter O\'Brian Junior' : 'Walter O\'Brian';
$createdAt = new \DateTime('now');
$githubName = $mock === 'normal-no-github' ? '' : ($mock === 'normal-long' ? 'sir-first-walterobrian-junior' : 'walterobrian');
$isHero = $mock === 'hero';
$isContributor = $mock === 'contributor';
$isEmployee = \str_starts_with($mock, 'employee');
$employeeNumber = match ($mock) {
'employee' => '1',
'employee-2digit' => '18',
default => ''
};
$isPlatinum = $mock === 'platinum';
}
if ($isEmployee) {
$isContributor = false;
$isHero = false;
}
if ($isHero) {
$isContributor = false;
$isEmployee = false;
}
if ($isContributor) {
$isHero = false;
$isEmployee = false;
}
$isGolden = $isEmployee || $isHero || $isContributor;
$isPlatinum = $isGolden ? false : $isPlatinum;
$memberSince = \strtoupper('Member since ' . $createdAt->format('M') . ' ' . $createdAt->format('d') . ', ' . $createdAt->format('o'));
$imagePath = $isGolden ? 'front-golden.png' : ($isPlatinum ? 'front-platinum.png' : 'front.png');
$baseImage = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/' . $imagePath);
if ($isEmployee) {
$image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/employee.png');
$image->setGravity(Imagick::GRAVITY_CENTER);
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 35);
$text = new ImagickDraw();
$text->setTextAlignment(Imagick::ALIGN_CENTER);
$text->setFont($this->getAppRoot() . '/public/fonts/Inter-Bold.ttf');
$text->setFillColor(new ImagickPixel('#FFFADF'));
$text->setFontSize(\strlen($employeeNumber) <= 2 ? 54 : 48);
$text->setFontWeight(700);
$metricsText = $baseImage->queryFontMetrics($text, $employeeNumber);
$hashtag = new ImagickDraw();
$hashtag->setTextAlignment(Imagick::ALIGN_CENTER);
$hashtag->setFont($this->getAppRoot() . '/public/fonts/Inter-Bold.ttf');
$hashtag->setFillColor(new ImagickPixel('#FFFADF'));
$hashtag->setFontSize(28);
$hashtag->setFontWeight(700);
$metricsHashtag = $baseImage->queryFontMetrics($hashtag, '#');
$startX = 898;
$totalWidth = $metricsHashtag['textWidth'] + 12 + $metricsText['textWidth'];
$hashtagX = ($metricsHashtag['textWidth'] / 2);
$textX = $hashtagX + 12 + ($metricsText['textWidth'] / 2);
$hashtagX -= $totalWidth / 2;
$textX -= $totalWidth / 2;
$hashtagX += $startX;
$textX += $startX;
$baseImage->annotateImage($hashtag, $hashtagX, 150, 0, '#');
$baseImage->annotateImage($text, $textX, 150, 0, $employeeNumber);
}
if ($isContributor) {
$image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/contributor.png');
$image->setGravity(Imagick::GRAVITY_CENTER);
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 34);
}
if ($isHero) {
$image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/hero.png');
$image->setGravity(Imagick::GRAVITY_CENTER);
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 34);
}
setlocale(LC_ALL, "en_US.utf8");
// $name = \iconv("utf-8", "ascii//TRANSLIT", $name);
// $memberSince = \iconv("utf-8", "ascii//TRANSLIT", $memberSince);
// $githubName = \iconv("utf-8", "ascii//TRANSLIT", $githubName);
$text = new ImagickDraw();
$text->setTextAlignment(Imagick::ALIGN_CENTER);
$text->setFont($this->getAppRoot() . '/public/fonts/Inter-Bold.ttf');
$text->setFillColor(new ImagickPixel('#FFFFFF'));
if (\strlen($name) > 32) {
$name = \substr($name, 0, 32);
}
if (\strlen($name) <= 23) {
$text->setFontSize(80);
$scalingDown = false;
} else {
$text->setFontSize(54);
$scalingDown = true;
}
$text->setFontWeight(700);
$baseImage->annotateImage($text, 512, 477, 0, $name);
$text = new ImagickDraw();
$text->setTextAlignment(Imagick::ALIGN_CENTER);
$text->setFont($this->getAppRoot() . '/public/fonts/Inter-SemiBold.ttf');
$text->setFillColor(new ImagickPixel($isGolden || $isPlatinum ? '#FFFFFF' : '#FFB9CC'));
$text->setFontSize(27);
$text->setFontWeight(600);
$text->setTextKerning(1.08);
$baseImage->annotateImage($text, 512, 541, 0, \strtoupper($memberSince));
if (!empty($githubName)) {
$text = new ImagickDraw();
$text->setTextAlignment(Imagick::ALIGN_CENTER);
$text->setFont($this->getAppRoot() . '/public/fonts/Inter-Regular.ttf');
$text->setFillColor(new ImagickPixel('#FFFFFF'));
$text->setFontSize($scalingDown ? 28 : 32);
$text->setFontWeight(400);
$metrics = $baseImage->queryFontMetrics($text, $githubName);
$baseImage->annotateImage($text, 512 + 20 + 4, 373 + ($scalingDown ? 2 : 0), 0, $githubName);
$image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/github.png');
$image->setGravity(Imagick::GRAVITY_CENTER);
$precisionFix = 5;
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 512 - ($metrics['textWidth'] / 2) - 20 - 4, 373 - ($metrics['textHeight'] - $precisionFix));
}
if (!empty($width) || !empty($height)) {
$baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
}
$response
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
->setContentType('image/png')
->file($baseImage->getImageBlob());
}
}
@@ -0,0 +1,428 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http\Cards\Cloud\OG;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Avatars\Http\Action;
use Appwrite\Utopia\Response;
use Imagick;
use ImagickDraw;
use ImagickPixel;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Logger\Logger;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getCloudCardOG';
}
public function __construct()
{
$this
->setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/cards/cloud-og')
->desc('Get OG image From Cloud Card')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resourceType', 'cards/cloud-og')
->label('cache.resource', 'card-og/{request.userId}')
->label('docs', false)
->label('origin', '*')
->param('userId', '', new UID(), 'User ID.', true)
->param('mock', '', new WhiteList(['employee', 'employee-2digit', 'hero', 'contributor', 'normal', 'platinum', 'normal-no-github', 'normal-long', 'normal-long-right', 'normal-long-middle', 'normal-bg2', 'normal-bg3', 'normal-right', 'normal-middle', 'platinum-right', 'platinum-middle', 'hero-middle', 'hero-right', 'contributor-right', 'employee-right', 'contributor-middle', 'employee-middle', 'employee-2digit-middle', 'employee-2digit-right']), 'Mocking behaviour.', true)
->param('width', 0, new Range(0, 1024), 'Resize image card width, Pass an integer between 0 to 1024.', true)
->param('height', 0, new Range(0, 1024), 'Resize image card height, Pass an integer between 0 to 1024.', true)
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('response')
->inject('heroes')
->inject('contributors')
->inject('employees')
->inject('logger')
->inject('authorization')
->callback($this->action(...));
}
public function action(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, Authorization $authorization)
{
$user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId));
if ($user->isEmpty() && empty($mock)) {
throw new Exception(Exception::USER_NOT_FOUND);
}
if (!$mock) {
$sequence = $user->getSequence();
$bgVariation = $sequence % 3 === 0 ? '1' : ($sequence % 3 === 1 ? '2' : '3');
$cardVariation = $sequence % 3 === 0 ? '1' : ($sequence % 3 === 1 ? '2' : '3');
$name = $user->getAttribute('name', 'Anonymous');
$email = $user->getAttribute('email', '');
$createdAt = new \DateTime($user->getCreatedAt());
$gitHub = $this->getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger, $authorization);
$githubName = $gitHub['name'] ?? '';
$githubId = $gitHub['id'] ?? '';
$isHero = \array_key_exists($email, $heroes);
$isContributor = \in_array($githubId, $contributors);
$isEmployee = \array_key_exists($email, $employees);
$employeeNumber = $isEmployee ? $employees[$email]['spot'] : '';
if ($isHero) {
$createdAt = new \DateTime($heroes[$email]['memberSince'] ?? '');
} elseif ($isEmployee) {
$createdAt = new \DateTime($employees[$email]['memberSince'] ?? '');
}
if (!$isEmployee && !empty($githubName)) {
$employeeGitHub = \array_search(\strtolower($githubName), \array_map(fn ($employee) => \strtolower($employee['gitHub']) ?? '', $employees));
if (!empty($employeeGitHub)) {
$isEmployee = true;
$employeeNumber = $isEmployee ? $employees[$employeeGitHub]['spot'] : '';
$createdAt = new \DateTime($employees[$employeeGitHub]['memberSince'] ?? '');
}
}
$isPlatinum = $user->getSequence() % 100 === 0;
} else {
$bgVariation = \str_ends_with($mock, '-bg2') ? '2' : (\str_ends_with($mock, '-bg3') ? '3' : '1');
$cardVariation = \str_ends_with($mock, '-right') ? '2' : (\str_ends_with($mock, '-middle') ? '3' : '1');
$name = \str_starts_with($mock, 'normal-long') ? 'Sir First Walter O\'Brian Junior' : 'Walter O\'Brian';
$createdAt = new \DateTime('now');
$githubName = $mock === 'normal-no-github' ? '' : (\str_starts_with($mock, 'normal-long') ? 'sir-first-walterobrian-junior' : 'walterobrian');
$isHero = \str_starts_with($mock, 'hero');
$isContributor = \str_starts_with($mock, 'contributor');
$isEmployee = \str_starts_with($mock, 'employee');
$employeeNumber = match ($mock) {
'employee' => '1',
'employee-right' => '1',
'employee-middle' => '1',
'employee-2digit' => '18',
'employee-2digit-right' => '18',
'employee-2digit-middle' => '18',
default => ''
};
$isPlatinum = \str_starts_with($mock, 'platinum');
}
if ($isEmployee) {
$isContributor = false;
$isHero = false;
}
if ($isHero) {
$isContributor = false;
$isEmployee = false;
}
if ($isContributor) {
$isHero = false;
$isEmployee = false;
}
$isGolden = $isEmployee || $isHero || $isContributor;
$isPlatinum = $isGolden ? false : $isPlatinum;
$memberSince = \strtoupper('Member since ' . $createdAt->format('M') . ' ' . $createdAt->format('d') . ', ' . $createdAt->format('o'));
$baseImage = new Imagick($this->getAppRoot() . "/public/images/cards/cloud/og-background{$bgVariation}.png");
$cardType = $isGolden ? '-golden' : ($isPlatinum ? '-platinum' : '');
$image = new Imagick($this->getAppRoot() . "/public/images/cards/cloud/og-card{$cardType}{$cardVariation}.png");
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 1008 / 2 - $image->getImageWidth() / 2, 1008 / 2 - $image->getImageHeight() / 2);
$imageLogo = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/og-background-logo.png');
$imageShadow = new Imagick($this->getAppRoot() . "/public/images/cards/cloud/og-shadow{$cardType}.png");
if ($cardVariation === '1') {
$baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 32, 1008 - $imageLogo->getImageHeight() - 32);
$baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -450, 700);
} elseif ($cardVariation === '2') {
$baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 1008 - $imageLogo->getImageWidth() - 32, 1008 - $imageLogo->getImageHeight() - 32);
$baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -20, 710);
} else {
$baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 1008 - $imageLogo->getImageWidth() - 32, 1008 - $imageLogo->getImageHeight() - 32);
$baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -135, 710);
}
if ($isEmployee) {
$file = $cardVariation === '3' ? 'employee-skew.png' : 'employee.png';
$image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/' . $file);
$image->setGravity(Imagick::GRAVITY_CENTER);
$hashtag = new ImagickDraw();
$hashtag->setTextAlignment(Imagick::ALIGN_LEFT);
$hashtag->setFont($this->getAppRoot() . '/public/fonts/Inter-Bold.ttf');
$hashtag->setFillColor(new ImagickPixel('#FFFADF'));
$hashtag->setFontSize(20);
$hashtag->setFontWeight(700);
$text = new ImagickDraw();
$text->setTextAlignment(Imagick::ALIGN_LEFT);
$text->setFont($this->getAppRoot() . '/public/fonts/Inter-Bold.ttf');
$text->setFillColor(new ImagickPixel('#FFFADF'));
$text->setFontSize(\strlen($employeeNumber) <= 1 ? 36 : 28);
$text->setFontWeight(700);
if ($cardVariation === '3') {
$hashtag->setFontSize(16);
$text->setFontSize(\strlen($employeeNumber) <= 1 ? 30 : 26);
$hashtag->skewY(20);
$hashtag->skewX(20);
$text->skewY(20);
$text->skewX(20);
}
$metricsHashtag = $baseImage->queryFontMetrics($hashtag, '#');
$metricsText = $baseImage->queryFontMetrics($text, $employeeNumber);
$group = new Imagick();
$groupWidth = $metricsHashtag['textWidth'] + 6 + $metricsText['textWidth'];
if ($cardVariation === '1') {
$group->newImage($groupWidth, $metricsText['textHeight'], '#00000000');
$group->annotateImage($hashtag, 0, $metricsText['textHeight'], 0, '#');
$group->annotateImage($text, $metricsHashtag['textWidth'] + 6, $metricsText['textHeight'], 0, $employeeNumber);
$image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1);
$image->rotateImage(new ImagickPixel('#00000000'), -20);
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203);
$group->rotateImage(new ImagickPixel('#00000000'), -22);
if (\strlen($employeeNumber) <= 1) {
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 660, 245);
} else {
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 655, 247);
}
} elseif ($cardVariation === '2') {
$group->newImage($groupWidth, $metricsText['textHeight'], '#00000000');
$group->annotateImage($hashtag, 0, $metricsText['textHeight'], 0, '#');
$group->annotateImage($text, $metricsHashtag['textWidth'] + 6, $metricsText['textHeight'], 0, $employeeNumber);
$image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1);
$image->rotateImage(new ImagickPixel('#00000000'), 30);
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425);
$group->rotateImage(new ImagickPixel('#00000000'), 32);
if (\strlen($employeeNumber) <= 1) {
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 775, 465);
} else {
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 767, 470);
}
} else {
$group->newImage(300, 300, '#00000000');
$hashtag->annotation(0, $metricsText['textHeight'], '#');
$text->annotation($metricsHashtag['textWidth'] + 2, $metricsText['textHeight'], $employeeNumber);
$group->drawImage($hashtag);
$group->drawImage($text);
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293);
if (\strlen($employeeNumber) <= 1) {
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 670, 317);
} else {
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 663, 322);
}
}
}
if ($isContributor) {
$file = $cardVariation === '3' ? 'contributor-skew.png' : 'contributor.png';
$image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/' . $file);
$image->setGravity(Imagick::GRAVITY_CENTER);
if ($cardVariation === '1') {
$image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1);
$image->rotateImage(new ImagickPixel('#00000000'), -20);
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203);
} elseif ($cardVariation === '2') {
$image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1);
$image->rotateImage(new ImagickPixel('#00000000'), 30);
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425);
} else {
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293);
}
}
if ($isHero) {
$file = $cardVariation === '3' ? 'hero-skew.png' : 'hero.png';
$image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/' . $file);
$image->setGravity(Imagick::GRAVITY_CENTER);
if ($cardVariation === '1') {
$image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1);
$image->rotateImage(new ImagickPixel('#00000000'), -20);
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203);
} elseif ($cardVariation === '2') {
$image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1);
$image->rotateImage(new ImagickPixel('#00000000'), 30);
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425);
} else {
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293);
}
}
setlocale(LC_ALL, "en_US.utf8");
// $name = \iconv("utf-8", "ascii//TRANSLIT", $name);
// $memberSince = \iconv("utf-8", "ascii//TRANSLIT", $memberSince);
// $githubName = \iconv("utf-8", "ascii//TRANSLIT", $githubName);
$textName = new ImagickDraw();
$textName->setTextAlignment(Imagick::ALIGN_CENTER);
$textName->setFont($this->getAppRoot() . '/public/fonts/Inter-Bold.ttf');
$textName->setFillColor(new ImagickPixel('#FFFFFF'));
if (\strlen($name) > 32) {
$name = \substr($name, 0, 32);
}
if ($cardVariation === '1') {
if (\strlen($name) <= 23) {
$scalingDown = false;
$textName->setFontSize(54);
} else {
$scalingDown = true;
$textName->setFontSize(36);
}
} elseif ($cardVariation === '2') {
if (\strlen($name) <= 23) {
$scalingDown = false;
$textName->setFontSize(50);
} else {
$scalingDown = true;
$textName->setFontSize(34);
}
} else {
if (\strlen($name) <= 23) {
$scalingDown = false;
$textName->setFontSize(44);
} else {
$scalingDown = true;
$textName->setFontSize(32);
}
}
$textName->setFontWeight(700);
$textMember = new ImagickDraw();
$textMember->setTextAlignment(Imagick::ALIGN_CENTER);
$textMember->setFont($this->getAppRoot() . '/public/fonts/Inter-Medium.ttf');
$textMember->setFillColor(new ImagickPixel($isGolden || $isPlatinum ? '#FFFFFF' : '#FFB9CC'));
$textMember->setFontWeight(500);
$textMember->setTextKerning(1.12);
if ($cardVariation === '1') {
$textMember->setFontSize(21);
$baseImage->annotateImage($textName, 550, 600, -22, $name);
$baseImage->annotateImage($textMember, 585, 635, -22, $memberSince);
} elseif ($cardVariation === '2') {
$textMember->setFontSize(20);
$baseImage->annotateImage($textName, 435, 590, 31.37, $name);
$baseImage->annotateImage($textMember, 412, 628, 31.37, $memberSince);
} else {
$textMember->setFontSize(16);
$textName->skewY(20);
$textName->skewX(20);
$textName->annotation(320, 700, $name);
$textMember->skewY(20);
$textMember->skewX(20);
$textMember->annotation(330, 735, $memberSince);
$baseImage->drawImage($textName);
$baseImage->drawImage($textMember);
}
if (!empty($githubName)) {
$text = new ImagickDraw();
$text->setTextAlignment(Imagick::ALIGN_LEFT);
$text->setFont($this->getAppRoot() . '/public/fonts/Inter-Regular.ttf');
$text->setFillColor(new ImagickPixel('#FFFFFF'));
$text->setFontSize($scalingDown ? 16 : 20);
$text->setFontWeight(400);
if ($cardVariation === '1') {
$metrics = $baseImage->queryFontMetrics($text, $githubName);
$group = new Imagick();
$groupWidth = $metrics['textWidth'] + 32 + 4;
$group->newImage($groupWidth, $metrics['textHeight'] + 10, '#00000000');
$image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/github.png');
$image->setGravity(Imagick::GRAVITY_CENTER);
$image->resizeImage(32, 32, Imagick::FILTER_LANCZOS, 1);
$precisionFix = -1;
$group->compositeImage($image, Imagick::COMPOSITE_OVER, 0, 0);
$group->annotateImage($text, 32 + 4, $metrics['textHeight'] - $precisionFix, 0, $githubName);
$group->rotateImage(new ImagickPixel('#00000000'), -22);
$x = 510 - $group->getImageWidth() / 2;
$y = 530 - $group->getImageHeight() / 2;
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, $x, $y);
} elseif ($cardVariation === '2') {
$metrics = $baseImage->queryFontMetrics($text, $githubName);
$group = new Imagick();
$groupWidth = $metrics['textWidth'] + 32 + 4;
$group->newImage($groupWidth, $metrics['textHeight'] + 10, '#00000000');
$image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/github.png');
$image->setGravity(Imagick::GRAVITY_CENTER);
$image->resizeImage(32, 32, Imagick::FILTER_LANCZOS, 1);
$precisionFix = -1;
$group->compositeImage($image, Imagick::COMPOSITE_OVER, 0, 0);
$group->annotateImage($text, 32 + 4, $metrics['textHeight'] - $precisionFix, 0, $githubName);
$group->rotateImage(new ImagickPixel('#00000000'), 31.11);
$x = 485 - $group->getImageWidth() / 2;
$y = 530 - $group->getImageHeight() / 2;
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, $x, $y);
} else {
$text->skewY(20);
$text->skewX(20);
$text->setTextAlignment(Imagick::ALIGN_CENTER);
$text->annotation(320 + 15 + 2, 640, $githubName);
$metrics = $baseImage->queryFontMetrics($text, $githubName);
$image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/github-skew.png');
$image->setGravity(Imagick::GRAVITY_CENTER);
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 512 - ($metrics['textWidth'] / 2), 518 + \strlen($githubName) * 1.3);
$baseImage->drawImage($text);
}
}
if (!empty($width) || !empty($height)) {
$baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
}
$response
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
->setContentType('image/png')
->file($baseImage->getImageBlob());
}
}
@@ -0,0 +1,64 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http\CreditCards;
use Appwrite\Platform\Modules\Avatars\Http\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getCreditCard';
}
public function __construct()
{
$this
->setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/avatars/credit-cards/:code')
->desc('Get credit card icon')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/credit-card')
->label('sdk', new Method(
namespace: 'avatars',
group: null,
name: 'getCreditCard',
description: '/docs/references/avatars/get-credit-card.md',
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
type: MethodType::LOCATION,
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::IMAGE_PNG
))
->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-credit-cards'))), 'Credit Card Code. Possible values: ' . \implode(', ', \array_keys(Config::getParam('avatar-credit-cards'))) . '.')
->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true)
->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true)
->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true)
->inject('response')
->callback($this->action(...));
}
public function action(string $code, int $width, int $height, int $quality, Response $response)
{
$this->avatar('credit-cards', $code, $width, $height, $quality, $response);
}
}
@@ -0,0 +1,216 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http\Favicon;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Avatars\Http\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\URL\URL as URLParse;
use Appwrite\Utopia\Response;
use DOMDocument;
use DOMElement;
use enshrined\svgSanitize\Sanitizer as SvgSanitizer;
use Utopia\Domains\Domain;
use Utopia\Fetch\Client;
use Utopia\Image\Image;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\URL;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getFavicon';
}
public function __construct()
{
$this
->setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/avatars/favicon')
->desc('Get favicon')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/favicon')
->label('sdk', new Method(
namespace: 'avatars',
group: null,
name: 'getFavicon',
description: '/docs/references/avatars/get-favicon.md',
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
type: MethodType::LOCATION,
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::IMAGE
))
->param('url', '', new URL(['http', 'https']), 'Website URL which you want to fetch the favicon from.')
->inject('response')
->callback($this->action(...));
}
public function action(string $url, Response $response)
{
$width = 56;
$height = 56;
$quality = 80;
$output = 'png';
$type = 'png';
if (!\extension_loaded('imagick')) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
}
$domain = new Domain(\parse_url($url, PHP_URL_HOST));
if (!$domain->isKnown()) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
}
$client = new Client();
try {
$res = $client
->setAllowRedirects(true)
->setMaxRedirects(5)
->setUserAgent(\sprintf(
APP_USERAGENT,
System::getEnv('_APP_VERSION', 'UNKNOWN'),
System::getEnv('_APP_EMAIL_SECURITY', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY))
))
->fetch($url);
} catch (\Throwable) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
}
$doc = new DOMDocument();
$doc->strictErrorChecking = false;
@$doc->loadHTML($res->getBody());
$links = $doc->getElementsByTagName('link') ?? [];
$outputHref = '';
$outputExt = '';
$space = 0;
foreach ($links as $link) { /* @var $link DOMElement */
$href = $link->getAttribute('href');
$rel = $link->getAttribute('rel');
$sizes = $link->getAttribute('sizes');
$absolute = URLParse::unparse(\array_merge(\parse_url($url), \parse_url($href)));
switch (\strtolower($rel)) {
case 'icon':
case 'shortcut icon':
//case 'apple-touch-icon':
$ext = \pathinfo(\parse_url($absolute, PHP_URL_PATH), PATHINFO_EXTENSION);
switch ($ext) {
case 'svg':
// SVG icons are prioritized by assigning the maximum possible value.
$space = PHP_INT_MAX;
$outputHref = $absolute;
$outputExt = $ext;
break;
case 'ico':
case 'png':
case 'jpg':
case 'jpeg':
$size = \explode('x', \strtolower($sizes));
$sizeWidth = (int) ($size[0] ?? 0);
$sizeHeight = (int) ($size[1] ?? 0);
if (($sizeWidth * $sizeHeight) >= $space) {
$space = $sizeWidth * $sizeHeight;
$outputHref = $absolute;
$outputExt = $ext;
}
break;
}
break;
}
}
if (empty($outputHref) || empty($outputExt)) {
$default = \parse_url($url);
$outputHref = $default['scheme'] . '://' . $default['host'] . '/favicon.ico';
$outputExt = 'ico';
}
$domain = new Domain(\parse_url($outputHref, PHP_URL_HOST));
if (!$domain->isKnown()) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
}
$client = new Client();
try {
$res = $client
->setAllowRedirects(true)
->setMaxRedirects(5)
->fetch($outputHref);
} catch (\Throwable) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
}
if ($res->getStatusCode() !== 200) {
throw new Exception(Exception::AVATAR_ICON_NOT_FOUND);
}
$data = $res->getBody();
if ('ico' === $outputExt) { // Skip crop, Imagick isn\'t supporting icon files
if (
empty($data) ||
stripos($data, '<html') === 0 ||
stripos($data, '<!doc') === 0
) {
throw new Exception(Exception::AVATAR_ICON_NOT_FOUND, 'Favicon not found');
}
$response
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
->setContentType('image/x-icon')
->file($data);
return;
}
if ('svg' === $outputExt) { // Skip crop, Imagick isn\'t supporting svg files
$sanitizer = new SvgSanitizer();
$sanitizer->minify(true);
$cleanSvg = $sanitizer->sanitize($data);
if ($cleanSvg === false) {
throw new Exception(Exception::AVATAR_SVG_SANITIZATION_FAILED);
}
$response
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
->setContentType('image/svg+xml')
->file($cleanSvg);
return;
}
$image = new Image($data);
$image->crop((int) $width, (int) $height);
$output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality);
$response
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
->setContentType('image/png')
->file($data);
unset($image);
}
}
@@ -0,0 +1,64 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http\Flags;
use Appwrite\Platform\Modules\Avatars\Http\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getFlag';
}
public function __construct()
{
$this
->setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/avatars/flags/:code')
->desc('Get country flag')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/flag')
->label('sdk', new Method(
namespace: 'avatars',
group: null,
name: 'getFlag',
description: '/docs/references/avatars/get-flag.md',
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
type: MethodType::LOCATION,
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::IMAGE_PNG
))
->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-flags'))), 'Country Code. ISO Alpha-2 country code format.')
->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true)
->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true)
->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true)
->inject('response')
->callback($this->action(...));
}
public function action(string $code, int $width, int $height, int $quality, Response $response)
{
$this->avatar('flags', $code, $width, $height, $quality, $response);
}
}
@@ -0,0 +1,107 @@
<?php
namespace Appwrite\Platform\Modules\Avatars\Http\Image;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Avatars\Http\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Domains\Domain;
use Utopia\Fetch\Client;
use Utopia\Image\Image;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Range;
use Utopia\Validator\URL;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getImage';
}
public function __construct()
{
$this
->setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/avatars/image')
->desc('Get image from URL')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/image')
->label('sdk', new Method(
namespace: 'avatars',
group: null,
name: 'getImage',
description: '/docs/references/avatars/get-image.md',
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
type: MethodType::LOCATION,
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::IMAGE
))
->param('url', '', new URL(['http', 'https']), 'Image URL which you want to crop.')
->param('width', 400, new Range(0, 2000), 'Resize preview image width, Pass an integer between 0 to 2000. Defaults to 400.', true)
->param('height', 400, new Range(0, 2000), 'Resize preview image height, Pass an integer between 0 to 2000. Defaults to 400.', true)
->inject('response')
->callback($this->action(...));
}
public function action(string $url, int $width, int $height, Response $response)
{
$quality = 80;
$output = 'png';
$type = 'png';
if (!\extension_loaded('imagick')) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
}
$domain = new Domain(\parse_url($url, PHP_URL_HOST));
if (!$domain->isKnown()) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
}
$client = new Client();
try {
$res = $client
->setAllowRedirects(false)
->fetch($url);
} catch (\Throwable) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
}
if ($res->getStatusCode() !== 200) {
throw new Exception(Exception::AVATAR_IMAGE_NOT_FOUND);
}
try {
$image = new Image($res->getBody());
} catch (\Throwable $exception) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unable to parse image');
}
$image->crop((int) $width, (int) $height);
$output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality);
$response
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
->setContentType('image/png')
->file($data);
unset($image);
}
}

Some files were not shown because too many files have changed in this diff Show More