Compare commits

...
Author SHA1 Message Date
Jake Barnby c961316f46 Fix params 2025-11-14 19:08:33 +13:00
Jake Barnby 2349401a86 Fix merge 2025-11-14 18:49:26 +13:00
Jake BarnbyandGitHub 6274d5a3e4 Merge branch '1.8.x' into refactor-auth-single-instance 2025-11-14 05:21:40 +00:00
Jake BarnbyandGitHub d0057ec404 Merge pull request #10800 from appwrite/feat-operators
Fix operators in transactions
2025-11-14 05:13:52 +00:00
Jake Barnby ab90563989 Fix tests 2025-11-14 17:31:17 +13:00
Jake BarnbyandGitHub e783f4c234 Merge pull request #10813 from appwrite/feat-csv-export
Feat csv export
2025-11-14 04:16:31 +00:00
Jake Barnby 83b9815644 Merge remote-tracking branch 'origin/1.8.x' into refactor-auth-single-instance 2025-11-14 16:45:57 +13:00
Jake Barnby afb0fcbc6f Update DB 2025-11-14 16:45:21 +13:00
Jake Barnby 082afcec19 Merge branch 'feat-operators' of github.com:appwrite/appwrite into feat-operators 2025-11-14 16:40:24 +13:00
Jake Barnby 2ab36f4cd6 Use helper for tests 2025-11-14 16:40:00 +13:00
Jake Barnby 72c71785a6 Fix doc 2025-11-14 16:35:10 +13:00
Jake BarnbyandGitHub 463eb0e66a Merge pull request #10785 from appwrite/dat-810
Added error message for the backups route
2025-11-14 03:30:35 +00:00
Jake Barnby 0588fb405d Fix keys 2025-11-14 16:16:06 +13:00
Jake Barnby b9211d7141 Add delete old CSV job for maintenance 2025-11-14 15:55:41 +13:00
Jake Barnby a11cf6b002 Merge remote-tracking branch 'origin/1.8.x' into feat-csv-export 2025-11-14 15:34:36 +13:00
Luke B. SilverandGitHub a2799b16f8 Merge pull request #10778 from appwrite/fix-nullable-validation
fix: null validation for optional params
2025-11-13 11:39:23 +00:00
Jake BarnbyandGitHub 298e6f9260 Merge branch '1.8.x' into feat-operators 2025-11-13 11:29:05 +00:00
Jake Barnby 3df2efb7ae Fix file reads 2025-11-14 00:15:18 +13:00
Jake BarnbyandGitHub b0a0454c55 Merge pull request #10738 from appwrite/fix-webp-library
Fix webp upload and previews
2025-11-13 11:01:10 +00:00
Jake Barnby b2d228e102 Update specs 2025-11-13 23:42:13 +13:00
Chirag Aggarwal b695ddc7e3 Merge branch '1.8.x' into fix-nullable-validation 2025-11-13 16:06:33 +05:30
Matej BačoandGitHub 5e4c74d501 Merge pull request #10486 from appwrite/feat-add-branch-deployments
Add branch-only deployments to Sites without ref tags
2025-11-13 10:33:46 +01:00
shimon 1da4736d5d composer.lock 2025-11-13 11:23:26 +02:00
Jake Barnby c78da61304 Merge remote-tracking branch 'origin/1.8.x' into feat-csv-export 2025-11-13 22:14:10 +13:00
shimon 5722241ff8 Merge branch '1.8.x' of github.com:appwrite/appwrite into refactor-auth-single-instance
# Conflicts:
#	composer.lock
2025-11-13 11:10:40 +02:00
Matej BačoandGitHub fe8593fecc Merge pull request #10751 from appwrite/ser-279
fix: Throw error when file token expiry is in the past
2025-11-13 09:34:46 +01:00
Damodar LohaniandGitHub b07674ccd3 Merge pull request #10812 from appwrite/fix-upgrade-utopia-database
Upgrade utopia-php database
2025-11-13 13:31:29 +05:45
Damodar Lohani aa5e3bf9a9 Merge remote-tracking branch 'origin/1.8.x' into fix-upgrade-utopia-database 2025-11-13 07:00:45 +00:00
Damodar Lohani c1b50e9941 Upgrade utopia-php database 2025-11-13 06:59:03 +00:00
Jake BarnbyandGitHub 3a76249d80 Merge pull request #10811 from appwrite/release-flutter-dart-add-screenshot-examples
release flutter + dart and add screenshot api docs
2025-11-13 06:58:32 +00:00
Chirag Aggarwal b2991709dc update version 2025-11-13 11:38:50 +05:30
Chirag Aggarwal fcc07533e1 update changelog 2025-11-13 11:34:35 +05:30
Chirag Aggarwal 3a6d907b42 release flutter + dart and add screenshot api docs 2025-11-13 11:29:38 +05:30
Atharva Deosthale d411b741b3 address comments 2025-11-12 23:06:18 +05:30
Hemachandar bc19e66775 fix tests 2025-11-12 20:27:42 +05:30
Hemachandar 08c9db7c60 use inline validator 2025-11-12 19:23:57 +05:30
Jake Barnby e8f2e78ac4 Use internal bucket 2025-11-12 22:18:25 +13:00
Jake Barnby 37f16ef0bb Fix array conversion 2025-11-12 21:54:37 +13:00
Jake Barnby 46d785bbb8 Fix tests 2025-11-12 21:04:17 +13:00
Damodar LohaniandGitHub a9c9f602ec Merge pull request #10807 from appwrite/feat-update-flutter-default-build-runtime
Update Flutter framework default build runtime version to 3.35
2025-11-12 13:16:20 +05:45
Damodar LohaniandGitHub d90b4c2f19 update framework config 2025-11-12 12:43:34 +05:45
Damodar LohaniandGitHub 79d733486c Update Flutter framework default build runtime version to 3.35 2025-11-12 12:35:31 +05:45
Damodar LohaniandGitHub d862245090 Merge pull request #10806 from appwrite/lohanidamodar-patch-2
Fix: add default fallback to Flutter framework
2025-11-12 12:30:11 +05:45
Damodar LohaniandGitHub aacaadcce0 Fix: add default fallback to Flutter framework 2025-11-12 12:17:15 +05:45
Damodar LohaniandGitHub 69ee6e0659 Merge pull request #10805 from appwrite/lohanidamodar-patch-2
Feat: enable Flutter 3.35 and Dart 3.9 for sites
2025-11-12 10:51:00 +05:45
Jake Barnby e564793089 Merge remote-tracking branch 'origin/1.8.x' into feat-operators
# Conflicts:
#	composer.lock
2025-11-12 17:02:58 +13:00
Jake Barnby 37910a4c5c Fix space 2025-11-12 16:59:50 +13:00
Jake Barnby 0c7551b0c1 Merge remote-tracking branch 'origin/1.8.x' into refactor-auth-single-instance
# Conflicts:
#	app/controllers/api/teams.php
#	composer.lock
2025-11-12 16:51:21 +13:00
Jake Barnby c9582978ed Fix operator parsing on txn ops 2025-11-12 16:48:42 +13:00
Jake Barnby ae21fadf0d Fix event firing on txn ops 2025-11-12 16:48:26 +13:00
Damodar LohaniandGitHub cfd62cf0a1 Update template-runtimes.php 2025-11-12 09:32:12 +05:45
Damodar LohaniandGitHub 279682941a enable dart 2025-11-12 09:31:04 +05:45
Damodar LohaniandGitHub eae26d90af Feat: enable Flutter 3.35 for sites 2025-11-12 09:25:35 +05:45
Jake BarnbyandGitHub b46ebb95a3 Merge pull request #10688 from appwrite/users-add-attributes
Users add new email attributes
2025-11-12 03:19:24 +00:00
Damodar LohaniandGitHub ffcb6a4677 Merge pull request #10804 from appwrite/feat-upgrade-runtimes
Feat: upgrade runtime
2025-11-12 07:26:17 +05:45
Damodar LohaniandGitHub 142e359dba Merge branch '1.8.x' into feat-upgrade-runtimes 2025-11-12 07:16:09 +05:45
Jake BarnbyandGitHub 4130983210 Merge pull request #10407 from appwrite/dat-647
updated migration error attribute size
2025-11-12 01:05:13 +00:00
Damodar Lohani e01ac65ce8 Feat: upgrade runtime 2025-11-12 00:26:27 +00:00
fogelito 97454c0768 EmailValidator chnage to email 2025-11-11 15:33:16 +02:00
fogelito 8b4a56cd9b $emailCanonical 2025-11-11 15:25:10 +02:00
fogelito da0a6b167f EmailValidator 2025-11-11 15:00:09 +02:00
fogelito 27d0c511ac patch users email 2025-11-11 12:09:40 +02:00
fogelito 8a58f7b6d7 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into users-add-attributes 2025-11-11 11:36:01 +02:00
fogelito cc97d78b5a Add try catch 2025-11-11 11:35:18 +02:00
Luke B. SilverandGitHub 91c24b671b Merge pull request #10791 from appwrite/release-cli-php
chore: release cli and php
2025-11-11 09:22:57 +00:00
fogelito b28e536967 Add try catch 2025-11-11 10:35:39 +02:00
Jake Barnby ab5aac00c4 Format 2025-11-11 21:25:21 +13:00
Jake Barnby 353e84bb74 Fix keys 2025-11-11 21:10:11 +13:00
Jake Barnby 94614d32d0 Handle deep nested ops 2025-11-11 21:09:12 +13:00
Jake Barnby 1efe9fa693 Merge remote-tracking branch 'origin/1.8.x' into feat-operators
# Conflicts:
#	composer.lock
2025-11-11 20:21:15 +13:00
Jake Barnby 74ff9a955b Fix transactions + operators 2025-11-11 20:20:08 +13:00
fogelito ce7f40dd34 Revert response model 2025-11-11 08:16:09 +02:00
fogelito 49318b5dac Merge branch '1.8.x' of https://github.com/appwrite/appwrite into users-add-attributes 2025-11-11 08:09:11 +02:00
Chirag AggarwalandGitHub da736631b2 Merge branch '1.8.x' into release-cli-php 2025-11-11 10:55:52 +05:30
Matej BačoandGitHub 9db6f9cb75 Merge pull request #10782 from appwrite/add-elevenlabs-site-template
Add ElevenLabs text-to-speech sites template
2025-11-10 16:35:26 +01:00
fogelito 6096c42ff9 More updates 2025-11-10 17:30:40 +02:00
Aditya Oberai b06354b99a update tanstack starter and elevenlabs template scores 2025-11-10 15:12:20 +00:00
Aditya OberaiandGitHub a797951bec Merge branch '1.8.x' into add-elevenlabs-site-template 2025-11-10 20:38:21 +05:30
bb0007a1e8 Update app/config/templates/site.php
Co-authored-by: Matej Bačo <matejbacocom@gmail.com>
2025-11-10 20:38:07 +05:30
Aditya Oberai 507717fdf7 generate screenshots from docker command 2025-11-10 14:49:44 +00:00
fogelito 5afae248ae Test Update 2025-11-10 15:00:18 +02:00
fogelito 98255fe628 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into users-add-attributes
# Conflicts:
#	composer.lock
2025-11-10 14:52:25 +02:00
Chirag Aggarwal 057397197b update to 18.0.1 2025-11-10 18:20:31 +05:30
Aditya Oberai c398d5fba9 Add default value for ELEVENLABS_API_KEY env variable 2025-11-10 12:17:29 +00:00
Chirag Aggarwal 4dc700927e fix: examples 2025-11-10 15:39:54 +05:30
Chirag Aggarwal 4a148e6ef9 remove broken execute 2025-11-10 15:06:22 +05:30
Chirag Aggarwal 2ff0ae61c8 update php 2025-11-10 15:00:09 +05:30
Chirag Aggarwal b06460b562 chore: release cli and php 2025-11-10 14:19:26 +05:30
Chirag Aggarwal 74492ded44 update description 2025-11-10 11:35:25 +05:30
Chirag AggarwalandGitHub 9ecffbf4e9 Merge branch '1.8.x' into fix-nullable-validation 2025-11-10 10:19:13 +05:30
Chirag Aggarwal 0ed4b5dbe7 fix imports + add spec changes 2025-11-10 10:16:58 +05:30
Chirag Aggarwal eb17ffd5d2 wrap all optional values with default null with nullable 2025-11-10 10:15:49 +05:30
Jake BarnbyandGitHub 0bf8cead51 Merge pull request #10789 from appwrite/sync-specs
chore: sync specs
2025-11-10 04:33:40 +00:00
Damodar LohaniandGitHub add1142d1f Merge pull request #10786 from appwrite/feat-function-sites-stats
Feat: stats sites and functions runtimes and frameworks
2025-11-10 09:52:36 +05:45
Chirag Aggarwal 86896a6d3c chore: sync specs 2025-11-10 08:51:56 +05:30
Damodar Lohani 4fd4af312d Fix array wrong reference 2025-11-09 08:18:37 +00:00
shimon 3ef6154ebe Update composer.lock to reflect package version upgrades and dependency changes, including Symfony components and Utopia PHP libraries. 2025-11-09 10:10:22 +02:00
Shimon NewmanandGitHub a96ba5499e Merge branch '1.8.x' into refactor-auth-single-instance 2025-11-09 10:06:03 +02:00
shimon 599aa1dcca Merge branch 'main' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-11-09 10:04:37 +02:00
Damodar Lohani 4b7a7496ff Feat: stats sites and functions runtimes and frameworks
- Sites frameworks count
- Functions runtimes count
2025-11-09 07:17:40 +00:00
ArnabChatterjee20k eb0e42b79c added error message for the backups route 2025-11-09 12:13:08 +05:30
Aditya OberaiandGitHub 0c93e7d545 Update app/config/templates/site.php 2025-11-07 23:29:36 +05:30
Aditya Oberai 469d0859fc Add ElevenLabs text-to-speech sites template 2025-11-07 17:07:25 +00:00
ArnabChatterjee20k 7c408d675e pr followups 2025-11-07 14:30:22 +05:30
Luke B. SilverandGitHub 4a96d89ee6 Merge pull request #10765 from appwrite/feat-sdk-release-runtime-config
Set configs for SDK release at runtime
2025-11-07 08:57:43 +00:00
ArnabChatterjee20k 4d47bc2ebf Merge remote-tracking branch 'upstream/1.8.x' into dat-647 2025-11-07 13:48:59 +05:30
ArnabChatterjee20k 119d6c1789 Merge remote-tracking branch 'upstream/1.8.x' into dat-647 2025-11-07 13:19:28 +05:30
Steven NguyenandGitHub 2aa2b8df91 chore: set GITHUB_TOKEN and git email at runtime 2025-11-07 00:06:01 +00:00
Steven NguyenandGitHub 4f1940ac0c Merge pull request #10739 from appwrite/copilot/sub-pr-10738
Add test coverage for webp upload and preview functionality
2025-11-06 15:07:52 -08:00
Steven NguyenandGitHub ef938a1c39 Merge pull request #10732 from appwrite/copilot/fix-google-token-disappears
Fix Google OAuth2 refresh token support
2025-11-06 10:28:02 -08:00
Steven NguyenandGitHub ebee879855 Merge pull request #10748 from appwrite/copilot/update-issue-triage-workflow
Migrate issue-triage workflow from event-based to scheduled batch processing with duplicate detection
2025-11-06 08:14:25 -08:00
shimon ae2ffd259c Remove debug output from HealthCustomServerTest 2025-11-06 17:54:44 +02:00
shimon 3d471bae56 health db debug 2025-11-06 17:37:31 +02:00
shimon d0f2293755 debug health db 2025-11-06 16:42:58 +02:00
shimon d6bd26da43 Merge branch 'refactor-auth-single-instance' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-11-06 16:12:02 +02:00
shimon 16d6c9e5e3 debug dbHelth 2025-11-06 16:11:57 +02:00
Matej BačoandGitHub e342009c9e Merge pull request #10771 from appwrite/Update-facts-on-Github-sites-and-functions
feat: Add new tips to Comment class
2025-11-06 13:50:27 +01:00
Matej BačoandGitHub 41d89f1b7a Merge pull request #10773 from appwrite/chore-remove-locale-cache
Chore: remove locale cache
2025-11-06 13:49:32 +01:00
Matej Bačo ad6aff6af2 remove locale cache 2025-11-06 13:24:17 +01:00
Harsh MahajanandGitHub 9b52f6f8f6 Add new tips to Comment class 2025-11-06 14:17:31 +05:30
Matej BačoandGitHub ad46d057d5 Merge pull request #10593 from appwrite/feat-SER-193-Update-facts-on-github-sites-and-functions
Feat: update facts on GitHub sites and functions
2025-11-06 09:35:53 +01:00
Damodar LohaniandGitHub 68ae8ce348 Merge pull request #10762 from appwrite/release-python
chore: release python sdk to 13.6.1
2025-11-06 09:24:43 +05:45
Chirag Aggarwal 87b0bfcdfe Merge branch '1.8.x' into release-python 2025-11-06 08:33:07 +05:30
copilot-swe-agent[bot]andstnguyen90 8a9d0a3f33 Rename logo-after.webp to image.webp
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-11-05 19:30:21 +00:00
Luke B. SilverandGitHub d37bab37e6 Merge pull request #10766 from appwrite/bump-database-3.1.5
Bump database version 3.1.5
2025-11-05 13:52:13 +00:00
fogelito 7810d54238 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into bump-database-3.1.5 2025-11-05 14:34:23 +02:00
Matej BačoandGitHub b3f9404b93 Merge pull request #10767 from appwrite/fix-tanstack-builds
Fix: tanstack builds crashing
2025-11-05 13:28:08 +01:00
fogelito d4b5d7df73 lock file 2025-11-05 13:54:07 +02:00
Matej Bačo e93f479157 Fix tanstack builds craashing 2025-11-05 12:48:39 +01:00
fogelito a1b1f97162 lock file 2025-11-05 13:36:43 +02:00
fogelito 1f0e3e0f51 lock file 2025-11-05 13:21:52 +02:00
shimon 26d3cef528 Merge branch '1.8.x' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-11-05 11:33:07 +02:00
shimon fb7691be3b Merge branch 'main' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-11-05 11:27:25 +02:00
Jake BarnbyandGitHub 501cba216b Update app/init/resources.php 2025-11-05 16:09:20 +13:00
Steven NguyenandGitHub 4ce2426c3d Merge pull request #10566 from Priyanshuthapliyal2005/fix/sitesCreateDeployment
fix: sites deployment activation instructions in createDeployment
2025-11-04 13:10:36 -08:00
Steven NguyenandGitHub 25b3680b1d Merge branch '1.8.x' into fix/sitesCreateDeployment 2025-11-04 11:36:37 -08:00
Steven NguyenandGitHub c910c98b32 chore: install ssh and gh commands for dev
This will allow creating github releases for SDKs
2025-11-04 16:18:49 +00:00
fogelito fe9d49c346 Response Model 2025-11-04 17:13:53 +02:00
shimon f3fd2fd191 composer.lock 2025-11-04 17:11:45 +02:00
Shimon NewmanandGitHub a970969c4d Merge branch '1.8.x' into refactor-auth-single-instance 2025-11-04 17:06:53 +02:00
Matej BačoandGitHub 6c1e4434a6 Merge pull request #10763 from appwrite/update-readme-file
Update README.md
2025-11-04 15:52:25 +01:00
shimon f0e4c9184d Update composer.json and composer.lock to reflect new version constraints for utopia-php/audit and utopia-php/database, ensuring compatibility with the latest releases. Adjusted versioning for utopia-php/migration and appwrite/sdk-generator to their respective latest versions. 2025-11-04 16:44:09 +02:00
VeereshandGitHub ba6e389f81 Update README.md 2025-11-04 19:33:30 +05:30
Chirag Aggarwal c442dd3ef1 chore: release python sdk to 13.6.1 2025-11-04 17:19:42 +05:30
Luke B. SilverandGitHub bf5aa7b86c Merge pull request #10761 from appwrite/feat-bump-utopia-dns
feat: bump utopia dns
2025-11-04 11:16:44 +00:00
loks0n b42cc11eba feat: bump utopia dns 2025-11-04 10:56:34 +00:00
Jake BarnbyandGitHub 79573b1d47 Merge pull request #10737 from appwrite/main
Sync main into 1.8.x
2025-11-04 19:58:18 +13:00
copilot-swe-agent[bot]andstnguyen90 2d332264c6 Replace with simple webp image for testing
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-11-04 06:16:41 +00:00
copilot-swe-agent[bot]andstnguyen90 2f843153da Add duplicate/related issues detection and listing to triage workflow
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-11-03 20:02:03 +00:00
copilot-swe-agent[bot]andstnguyen90 df8fb23e75 Fix test assertion for file count with offset
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-11-03 18:13:21 +00:00
Hemachandar 60e57eb9c5 lint 2025-11-03 17:29:48 +05:30
Hemachandar 10a9845898 fix: Throw error when file token expiry is in the past 2025-11-03 16:55:13 +05:30
shimon dc43b5977f Inject authorization dependency into the Update class to enhance authorization handling for string column updates. 2025-11-03 12:58:22 +02:00
shimon e1fbaf14eb Inject authorization dependency into the Update class for improved authorization handling in relationship updates. 2025-11-03 12:51:21 +02:00
shimon d721425cfe composer.lock 2025-11-03 12:33:07 +02:00
shimon 02dd275be4 Update composer.lock with new reference for utopia-php/database and modify Upsert action method to remove unused Authorization parameter and enhance document validation. This improves code clarity and maintainability. 2025-11-03 11:54:50 +02:00
shimon 3391563829 Remove unused Authorization import from Bulk Delete.php to clean up code and improve maintainability. 2025-11-03 11:07:17 +02:00
shimon 5709265f6c linter 2025-11-03 11:02:30 +02:00
shimon 89d5a28c28 Refactor authorization handling to consistently utilize the Authorization class across multiple controllers and functions. Update method signatures to include Authorization as a parameter, ensuring cleaner and more maintainable code. This change enhances the overall structure and readability of the authorization logic. 2025-11-03 10:37:05 +02:00
shimon f613a06d19 composer.lock 2025-11-03 10:04:01 +02:00
Shimon NewmanandGitHub 30f4b846d2 Merge branch '1.8.x' into refactor-auth-single-instance 2025-11-03 09:21:13 +02:00
shimon a3742bc65e compose.lock 2025-11-03 09:13:04 +02:00
shimon cd1b2b6c05 Refactor authorization handling by replacing method calls with direct class references, ensuring consistent usage of the Authorization class across various modules. Update method signatures to include Authorization as a parameter where necessary, and remove unnecessary injections. Additionally, correct the documentation in CHANGES.md for clarity. 2025-11-03 09:07:44 +02:00
Jake BarnbyandGitHub 55103a1d04 Merge pull request #10750 from appwrite/feat-csv-export
CSV export email updates
2025-11-03 19:26:49 +13:00
Jake BarnbyandGitHub 6e71a3ca4c Merge branch '1.8.x' into feat-csv-export 2025-11-03 19:26:25 +13:00
Jake BarnbyandGitHub 1f173eb3c3 Merge pull request #10735 from appwrite/feat-operators
Add operator support
2025-11-03 19:25:44 +13:00
Jake Barnby 2db205d186 Update changelogs 2025-11-03 19:06:16 +13:00
Jake Barnby 1d84f1666d Fix copy 2025-11-03 18:36:07 +13:00
Jake Barnby aa6390062f Fix text centering 2025-11-03 18:35:59 +13:00
Jake Barnby 455b9b3ca7 Fix logo size 2025-11-03 18:35:43 +13:00
Jake Barnby 45ead583c6 Generate docs 2025-11-03 17:03:45 +13:00
Jake Barnby 750ccdf33d Merge remote-tracking branch 'origin/1.8.x' into feat-operators 2025-11-03 16:06:09 +13:00
shimon c13aed651a Refactor storage file retrieval to use queries directly for counting, improving consistency in authorization handling. Update Action class to remove unnecessary authorization skip, and include authorization in Email/Create class constructor for better dependency management. 2025-11-02 21:24:11 +02:00
shimon dbccb16784 linter 2025-11-02 19:01:59 +02:00
shimon c0be333c0f Merge branch 'refactor-auth-single-instance' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-11-02 18:58:51 +02:00
shimon d96c7eb3e9 Enhance TransactionState to utilize Authorization class for improved transaction management. Updated method signatures and resource initialization to include authorization, ensuring consistent handling across the application. 2025-11-02 18:58:41 +02:00
Shimon NewmanandGitHub 4fca374fe9 Merge branch '1.8.x' into refactor-auth-single-instance 2025-11-02 18:15:26 +02:00
shimon 9b82dd0411 Update composer.lock to reflect version upgrades for utopia-php/detector (0.2.1 to 0.2.2) and appwrite/sdk-generator (1.4.16 to 1.5.0), including updated references and source URLs. 2025-11-02 18:14:31 +02:00
shimon 0904881de8 Refactor authorization handling to ensure consistent usage of the Authorization class across various modules, enhancing code maintainability and clarity. Update method signatures to include authorization as a parameter where necessary. 2025-11-02 18:11:56 +02:00
fogelito 6a41f8b2b9 Remove todos 2025-11-02 11:16:12 +02:00
fogelito 503145734b Move user Document out of try block 2025-11-02 11:06:56 +02:00
fogelito 371cab0658 try catch EmailCanonical 2025-11-02 10:59:28 +02:00
fogelito ca8cb6f260 Add utopia-php/emails 2025-11-02 09:13:55 +02:00
Eldad A. FuxandGitHub 745e9e2bea Merge pull request #10675 from appwrite/feat-screenshots-endpoint
POC - website screenshots
2025-11-01 18:03:37 +01:00
copilot-swe-agent[bot]andstnguyen90 1f9afc8cab Update issue-triage workflow to run daily and process last 24h issues
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-11-01 15:47:43 +00:00
copilot-swe-agent[bot] c5d4551cab Initial plan 2025-11-01 15:39:50 +00:00
Matej BačoandGitHub 230148c06e Merge pull request #10747 from appwrite/feat-next-js-standalone
Feat: Next.js standalone support
2025-10-31 16:26:32 +01:00
Matej Bačo 3197ebb655 Merge branch '1.8.x' into feat-next-js-standalone 2025-10-31 13:56:55 +01:00
Matej Bačo af3a0bb34b Update detection lib 2025-10-31 13:51:38 +01:00
Matej BačoandGitHub ea1099b112 Merge pull request #10746 from appwrite/ser-453
Update base template for session alert email
2025-10-31 13:47:22 +01:00
Hemachandar f9542e2be2 Update base template for session alert email 2025-10-31 17:52:47 +05:30
Jake Barnby 3e291492fb Update versions 2025-11-01 01:16:24 +13:00
Jake Barnby e7dc7a6fba Update specs 2025-11-01 01:05:15 +13:00
Luke B. SilverandGitHub b525605d1b Merge pull request #10745 from appwrite/sdk-release-doc
doc: add tutorial for releasing sdks
2025-10-31 12:01:48 +00:00
Jake Barnby dc968a1fa4 Merge remote-tracking branch 'origin/1.8.x' into feat-operators
# Conflicts:
#	composer.lock
2025-11-01 00:56:06 +13:00
Chirag Aggarwal ebee3536f5 final changes 2025-10-31 17:24:03 +05:30
Chirag Aggarwal 8c8073d7aa grammer fixes 2025-10-31 17:22:02 +05:30
Jake Barnby d284c29e2a Update generator 2025-11-01 00:50:36 +13:00
Jake Barnby 0fae37a999 Fix colors 2025-11-01 00:49:08 +13:00
Chirag Aggarwal fce4f72ca9 doc: add tutorial for releasing sdks 2025-10-31 17:18:54 +05:30
Jake Barnby 77968be454 Fix template 2025-11-01 00:10:39 +13:00
Luke B. SilverandGitHub 4f905e06e2 Merge pull request #10744 from appwrite/regen-sdks
regen sdks
2025-10-31 09:34:52 +00:00
Chirag Aggarwal ed39fd7999 regen sdks 2025-10-31 14:31:16 +05:30
Jake BarnbyandGitHub 71cd882324 Merge pull request #10743 from appwrite/update-sdk-gen
update sdk gen
2025-10-31 21:54:43 +13:00
Chirag Aggarwal 68d7ff9c55 update sdk gen 2025-10-31 13:05:29 +05:30
Jake BarnbyandGitHub 8ca083ed27 Merge pull request #10729 from appwrite/update-sdks-2
update sdks
2025-10-31 19:54:45 +13:00
Jake Barnby 600da2a504 Add operator support 2025-10-31 11:47:20 +13:00
copilot-swe-agent[bot]andstnguyen90 d1511d5ba1 Refactor webp tests per review feedback
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-10-30 22:26:30 +00:00
copilot-swe-agent[bot]andstnguyen90 4a8651ab39 Add webp test cases for upload/view and preview
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-10-30 21:58:02 +00:00
copilot-swe-agent[bot]andstnguyen90 de4f473da1 Initial plan for adding webp test cases
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-10-30 21:56:12 +00:00
copilot-swe-agent[bot] ce539972a5 Initial plan 2025-10-30 21:51:50 +00:00
Steven NguyenandGitHub 458100cd69 Upgrade base image to version 0.10.5
Ensure libwebp is installed in the base image when imagemagick is installed so that imagemagick works with webp.
2025-10-30 14:42:18 -07:00
fogelito fea1630909 Remove redundant emailVerification 2025-10-30 18:18:09 +02:00
fogelito e40e88c322 Update emails attributes 2025-10-30 18:09:36 +02:00
Chirag Aggarwal 6bd52875dc update to major 2025-10-30 18:13:32 +05:30
Chirag Aggarwal 88dffcffbf format 2025-10-30 11:01:42 +05:30
Chirag Aggarwal 07214de370 update sdks 2025-10-30 10:03:36 +05:30
Damodar Lohani d56bd44f67 Fix label parsing 2025-10-30 02:07:15 +00:00
Damodar LohaniandGitHub a39970c59a Merge branch '1.8.x' into feat-screenshots-endpoint 2025-10-30 07:26:43 +05:45
Damodar Lohani b30890c1d6 remove duplicate configs 2025-10-30 01:37:53 +00:00
Damodar Lohani 5c1f6244c8 remove dump 2025-10-30 01:32:22 +00:00
Damodar Lohani 7156036120 remove viewport fullpage duplicate 2025-10-30 01:31:45 +00:00
copilot-swe-agent[bot]andstnguyen90 627adb43e0 Add access_type=offline and prompt=consent to Google OAuth2 login URL to enable refresh tokens
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-10-29 23:12:28 +00:00
copilot-swe-agent[bot] 9ad08694ed Initial plan 2025-10-29 23:01:33 +00:00
Shimon NewmanandGitHub d38399dcc0 Merge branch '1.8.x' into refactor-auth-single-instance 2025-10-29 20:38:04 +02:00
shimon 68b0fe8c70 linter 2025-10-29 20:33:36 +02:00
shimon 7c2a1ade1f Merge branch 'refactor-auth-single-instance' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-10-29 20:22:00 +02:00
shimon 5c2828bc78 Refactor authorization handling across multiple modules to use a single instance of the Authorization class. 2025-10-29 20:21:41 +02:00
111b2abeeb Update app/cli.php
Co-authored-by: Jake Barnby <jakeb994@gmail.com>
2025-10-29 20:19:49 +02:00
Steven NguyenandGitHub a869ae3323 Merge pull request #10725 from appwrite/copilot/analyze-action-failure
Add missing models: read permission to AI Moderator workflow
2025-10-29 07:33:44 -07:00
Chirag Aggarwal baa7a170d9 update sdks 2025-10-29 18:54:29 +05:30
Luke B. SilverandGitHub 60726757ab Merge pull request #10726 from appwrite/fix-testListSites
fix: testListSites 2 not matching expected 1
2025-10-29 12:48:38 +00:00
Jake BarnbyandGitHub 7b55a9810f Merge pull request #10668 from appwrite/feat-disable-count
Feat disable count
2025-10-29 23:47:33 +13:00
Chirag Aggarwal d995dbb159 fix: naming 2025-10-29 15:19:16 +05:30
Chirag Aggarwal 1180dc0781 use search 2025-10-29 15:13:33 +05:30
Chirag Aggarwal 2951a1b7b8 fix: testListSites 2 not matching expected 1 2025-10-29 15:11:42 +05:30
Shimon NewmanGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
9503507011 Update src/Appwrite/Utopia/Response.php
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-10-29 11:20:49 +02:00
shimon a7dbb3daea Refactor API parameters to replace 'includeTotal' with 'total' across multiple endpoints for consistency in response handling. 2025-10-29 11:08:08 +02:00
shimon a6d1f46133 addressing comments 2025-10-29 10:38:09 +02:00
Steven NguyenandGitHub e5cd093a7b Apply suggestion from @stnguyen90 2025-10-29 00:45:49 -07:00
shimon 33000134df addrising comments + composer.lock 2025-10-29 09:17:10 +02:00
copilot-swe-agent[bot]andstnguyen90 2078e52798 Add contents: read permission to AI Moderator workflow
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-10-29 07:16:52 +00:00
copilot-swe-agent[bot] 08c51b8761 Initial plan 2025-10-29 07:12:09 +00:00
Steven NguyenandGitHub 37dfce0f5b Merge pull request #10718 from appwrite/add-workflow-workflows-issue-triage.md-5094
Add agentic workflow issue-triage
2025-10-29 00:04:22 -07:00
shimon b893599ba8 Merge branch '1.8.x', commit 'f18aa6b6486d52ef97674c1ac483c476b85a6f21' of github.com:appwrite/appwrite into feat-disable-count
# Conflicts:
#	composer.lock
2025-10-29 09:03:44 +02:00
Damodar LohaniandGitHub 8f87b9cb5e Merge branch '1.8.x' into feat-screenshots-endpoint 2025-10-29 09:19:47 +05:45
Damodar Lohani b303bd1051 add correct assertion 2025-10-29 03:34:25 +00:00
Damodar Lohani 60a4266204 validate data length 2025-10-29 03:30:59 +00:00
Damodar Lohani 637bf4f231 add abuse limit 2025-10-29 03:30:43 +00:00
Steven NguyenandGitHub 92f776b525 Merge pull request #10717 from appwrite/copilot/add-ai-moderator-repo
Add GitHub AI Moderator workflow
2025-10-28 11:46:35 -07:00
shimon 145b884628 compose.lock 2025-10-28 15:07:26 +02:00
shimon 0e22b06f0a linter 2025-10-28 15:01:08 +02:00
shimon 9e79d883a0 Refactor authorization retrieval in realtime and update imports in migration files; clean up whitespace in tests 2025-10-28 14:16:26 +02:00
shimon 61de3b9e08 Merge branch 'refactor-auth-single-instance' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-10-28 11:38:08 +02:00
Shimon NewmanandGitHub 734c44c1da Merge branch '1.8.x' into refactor-auth-single-instance 2025-10-28 11:30:43 +02:00
shimon 68524067cf migrations tests fix 2025-10-28 11:29:14 +02:00
shimon cc6319a2c7 singletone fix 2025-10-28 11:09:20 +02:00
Jake BarnbyandGitHub 2a260c3d5f Merge pull request #10720 from appwrite/composer-add-emails
Composer add emails lib
2025-10-28 22:05:19 +13:00
fogelito b8321d3bb0 Update lock for database 3.0.4 2025-10-28 10:25:29 +02:00
Shimon NewmanandGitHub dee3d375db Merge branch '1.8.x' into refactor-auth-single-instance 2025-10-28 10:01:33 +02:00
fogelito 66610cfd4e Pull main 2025-10-28 09:33:56 +02:00
fogelito 149fa141d0 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into composer-add-emails
# Conflicts:
#	composer.lock
2025-10-28 09:33:21 +02:00
fogelito 9305c86367 Add utopia-php/emails 0.6 2025-10-28 09:32:00 +02:00
shimon 462634d311 Update dependencies in composer.json and composer.lock; upgrade utopia-php/detector to version 0.2.*, and utopia-php/migration to version 1.3.3. Modify exception messages in storage API for better clarity on authorization failures. Adjust test assertions to reflect updated error messages. 2025-10-28 09:09:54 +02:00
Jake BarnbyandGitHub 0e6d3279d7 Merge pull request #10546 from appwrite/feat-csv-export
CSV export
2025-10-28 18:19:54 +13:00
Jake Barnby c0cb4681ae Fix error encode 2025-10-28 17:58:31 +13:00
Jake BarnbyandGitHub b9fb10a379 Merge pull request #10719 from appwrite/fix-error-handler-error
Return empty on no collection, fallback to route validation
2025-10-28 17:39:05 +13:00
Jake Barnby b39a118bba Clean up errors 2025-10-28 17:37:22 +13:00
Jake Barnby 9f2ff0e1f7 Merge remote-tracking branch 'origin/1.8.x' into feat-csv-export
# Conflicts:
#	app/config/specs/open-api3-1.8.x-client.json
#	app/config/specs/open-api3-1.8.x-console.json
#	app/config/specs/open-api3-1.8.x-server.json
#	app/config/specs/open-api3-latest-client.json
#	app/config/specs/open-api3-latest-console.json
#	app/config/specs/open-api3-latest-server.json
#	app/config/specs/swagger2-1.8.x-client.json
#	app/config/specs/swagger2-1.8.x-console.json
#	app/config/specs/swagger2-1.8.x-server.json
#	app/config/specs/swagger2-latest-client.json
#	app/config/specs/swagger2-latest-console.json
#	app/config/specs/swagger2-latest-server.json
2025-10-28 17:22:02 +13:00
Jake Barnby 15cabd018d Lint 2025-10-28 16:56:54 +13:00
Jake Barnby 8a5a6037b1 Return empty on no collection, fallback to route validation 2025-10-28 16:50:38 +13:00
Steven NguyenandGitHub eec3d210ee Add agentic workflow issue-triage 2025-10-28 03:00:30 +00:00
copilot-swe-agent[bot]andstnguyen90 b729c8119d Fix parameter name to use 'token' instead of 'github-token'
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-10-28 02:18:07 +00:00
copilot-swe-agent[bot]andstnguyen90 30a093a0d5 Add GitHub AI Moderator workflow
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-10-28 02:16:40 +00:00
copilot-swe-agent[bot] dee3ddffca Initial plan 2025-10-28 02:11:53 +00:00
shimon e769cf4e48 Remove vendor path for utopia-php/database in docker-compose.yml 2025-10-27 21:23:03 +02:00
shimon fbaffa980a linter 2025-10-27 20:46:58 +02:00
shimon 26f61c1855 add back registers.php 2025-10-27 19:34:49 +02:00
shimon dae62d863a Merge remote-tracking branch 'origin/refactor-auth-single-instance' into refactor-auth-single-instance 2025-10-27 19:27:33 +02:00
shimon 681fa198c9 composer.lock 2025-10-27 19:27:16 +02:00
shimon e7b8e6c90d Merge branch '1.8.x' of github.com:appwrite/appwrite into refactor-auth-single-instance
# Conflicts:
#	composer.json
#	composer.lock
2025-10-27 19:26:53 +02:00
shimon d7cc3895cb Update composer.lock to reflect version changes for utopia-php/detector, remove unused registers.php file, and clean up whitespace in cli.php and worker.php. Refactor database resource initialization in resources.php for consistency. 2025-10-27 19:23:55 +02:00
Matej BačoandGitHub dde6968e50 Merge pull request #10713 from appwrite/fix-next-16
Fix: Next.js 16
2025-10-27 17:22:57 +01:00
Matej Bačo 71349385b4 Upgrade Console (tanstack support), and detector (next.js 16) 2025-10-27 17:03:24 +01:00
Shimon NewmanandGitHub a0b21a0bd4 Merge branch '1.8.x' into refactor-auth-single-instance 2025-10-27 16:24:08 +02:00
shimon 65e99ff0de Merge remote-tracking branch 'origin/refactor-auth-single-instance' into refactor-auth-single-instance 2025-10-27 16:20:28 +02:00
shimon ecd6880fe0 composer.lock 2025-10-27 16:20:07 +02:00
shimon e20adbba61 Merge branch 'main' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-10-27 16:18:50 +02:00
shimon 30d58e6371 Update composer dependencies for utopia-php/audit and utopia-php/database, reflecting new versioning and aliases. Refactor document validation in Create operation to utilize the new authorization method for better security handling. 2025-10-27 16:14:04 +02:00
fogelito 037d60633b Add emailCanonical 2025-10-27 15:59:20 +02:00
fogelito 6f299e922c Add emails 2025-10-27 15:50:45 +02:00
Luke B. SilverandGitHub 7047ffe3dd Merge pull request #10707 from appwrite/update-docs-examples-to-use-permission-class
Update docs examples to use permission class
2025-10-27 12:38:13 +00:00
Chirag Aggarwal 73e3f2869f fix: imports 2025-10-27 17:41:33 +05:30
fogelito 218bb4c537 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into users-add-attributes 2025-10-27 13:37:33 +02:00
Luke B. SilverandGitHub e4f63ed0c9 Merge pull request #10694 from appwrite/update-sdks
update sdks
2025-10-27 11:16:26 +00:00
shimon 6ed2000478 Update composer.lock and refactor authorization handling in CLI and worker modules. Adjust Docker configuration to include database source path. Modify error handling in general controller and enhance permission setting in document creation. Fix privileged user role retrieval in function execution. Ensure consistent authorization usage across various resources. 2025-10-27 12:09:23 +02:00
Chirag Aggarwal 720dfb97e8 feat: improve doc examples to use Permission and Roles helper classes 2025-10-27 14:13:54 +05:30
shimon f18aa6b648 composer.lock 2025-10-27 10:14:41 +02:00
shimon b760a88af7 Merge branch '1.8.x', commit 'ab1e51034826dde50034e6e73fc5d546e800dd6d' of github.com:appwrite/appwrite into feat-disable-count
# Conflicts:
#	composer.lock
2025-10-27 10:13:37 +02:00
Damodar LohaniandGitHub 74bcfe1e08 Merge pull request #10706 from appwrite/feat-screenshot-endpoint-stats
Feat: usage stats for screenshot generated
2025-10-27 12:57:48 +05:45
Damodar Lohani 2afba50a8c Feat: usage stats for screenshot generated 2025-10-27 06:44:04 +00:00
Damodar LohaniandGitHub 15b12551a0 Merge pull request #10702 from appwrite/fix-description
fix: execution status enum description
2025-10-27 12:08:20 +05:45
Chirag Aggarwal 22f44c51d6 regen specs 2025-10-27 11:23:56 +05:30
Damodar LohaniandGitHub 128dc34a52 Merge pull request #10705 from appwrite/fix-warning-builds-worker
Fix warning builds worker
2025-10-27 10:42:29 +05:45
Chirag Aggarwal ecc3f61f1e fix: warning Undefined variable $adapter 2025-10-27 08:41:13 +05:30
eldadfux 61f4c7957a Add new configuration variable '_APP_BROWSER_HOST' for browser service communication; update avatar screenshot API to use System::getEnv for environment variable retrieval 2025-10-25 10:22:53 +01:00
Chirag Aggarwal ffb2a02ea0 fix: execution status enum description 2025-10-25 10:52:22 +05:30
Chirag Aggarwal f6135a0e04 update composer 2025-10-25 10:10:51 +05:30
eldadfux f13c029751 Refactor avatar screenshot API to replace 'viewport' parameter with separate 'viewportWidth' and 'viewportHeight' parameters; update related tests for validation of new dimensions and edge cases. 2025-10-24 22:53:10 +01:00
Steven NguyenandGitHub 6ded8dce67 Merge pull request #10700 from appwrite/chore-issue-auto-labeler
Create workflow to auto add labels to issues
2025-10-24 14:50:46 -07:00
Eldad Fux d8bde641e9 Update appwrite-browser image to version 0.3.1; enhance avatar screenshot API by adding 'waitUntil' parameter and refactoring image processing logic for improved efficiency. 2025-10-24 21:42:10 +01:00
Steven NguyenandSteven Nguyen 21b95c2645 chore: create workflow to auto add labels to issues 2025-10-24 11:24:57 -07:00
Matej BačoandGitHub 3733df6fc5 Merge pull request #10698 from appwrite/ser-218-functions
Fix errors on downloading function deployment output
2025-10-24 16:57:48 +02:00
Hemachandar b5d26bb426 Fix errors on downloading function deployment output 2025-10-24 19:59:48 +05:30
Matej Bačo fa4529846c Applying SDK generation fix for Console
https://github.com/appwrite/appwrite/pull/10628/files
2025-10-24 16:18:11 +02:00
Matej BačoandGitHub 1fc790802d Merge pull request #10681 from appwrite/feat-tanstack-start-sites
Feat: Tanstack support
2025-10-24 15:57:20 +02:00
Matej Bačo 52ababa800 Update docker-compose.yml 2025-10-24 14:57:12 +02:00
Matej Bačo b3c1dea8d7 Fix template test 2025-10-24 13:25:25 +02:00
Matej Bačo f51c387cd8 Update composer.lock 2025-10-24 12:44:42 +02:00
Matej Bačo 3dd1c3de13 Merge branch '1.8.x' into feat-tanstack-start-sites 2025-10-24 12:43:51 +02:00
Matej Bačo 4472437696 AI code review fix 2025-10-24 12:41:46 +02:00
Matej Bačo ca39c2cde5 Add tanstack starter 2025-10-24 12:33:50 +02:00
Chirag Aggarwal a7b74cede8 update generator 2025-10-24 14:46:28 +05:30
Jake BarnbyandGitHub 24bfad100d Merge pull request #10690 from appwrite/feat-resend
feat: resend adapter
2025-10-24 09:13:24 +00:00
Matej BačoandGitHub a8de338d5f Merge pull request #10633 from appwrite/ser-376
Dark mode styles for emails
2025-10-24 10:50:53 +02:00
Chirag Aggarwal 9c0772f125 add tests 2025-10-24 14:19:59 +05:30
Chirag Aggarwal d90e2621a1 update sdks 2025-10-24 14:00:39 +05:30
HemachandarandGitHub 572870e320 Merge branch '1.8.x' into ser-376 2025-10-24 13:55:21 +05:30
Matej BačoandGitHub 046d1190cf Merge pull request #10691 from appwrite/chore-skip-auth-to-delete-vcs-lock
Skip auth check to delete vcs lock document
2025-10-23 18:00:58 +02:00
Khushboo Verma a6af0cc73a Skip auth check to delete vcs lock document 2025-10-23 20:27:45 +05:30
Harsh MahajanandGitHub 1468231de7 Update Comment.php 2025-10-23 20:20:16 +05:30
Chirag Aggarwal f12481ce20 add update endpoint 2025-10-23 19:42:12 +05:30
Chirag Aggarwal a280a565f6 feat: resend adapter 2025-10-23 19:06:11 +05:30
Matej BačoandGitHub c80b40cec7 Merge pull request #10648 from appwrite/ser-218
Fix errors on downloading large deployment output
2025-10-23 14:25:34 +02:00
HemachandarandGitHub 5ebb7e23a9 Merge branch '1.8.x' into ser-218 2025-10-23 17:31:16 +05:30
Matej Bačo 6f188a03d5 Manual QA bug fixing 2025-10-23 12:43:57 +02:00
fogelito c13a70e226 Add users attributes 2025-10-23 12:38:18 +03:00
Jake Barnby 7318ebf1d8 Merge branch 'feat-csv-export' of github.com:appwrite/appwrite into feat-csv-export 2025-10-23 21:34:21 +13:00
Jake Barnby e222221ec9 Add failure email 2025-10-23 21:34:09 +13:00
Jake Barnby fdf5367e39 Fix options not fired from realtime 2025-10-23 20:35:17 +13:00
DarshanandGitHub 42c75c43de Merge branch '1.8.x' into feat-csv-export 2025-10-23 12:20:06 +05:30
Jake Barnby 28a6f6de00 Format 2025-10-23 18:57:10 +13:00
Jake Barnby c03b56c0de Check plan file size max 2025-10-23 18:47:55 +13:00
Jake Barnby e21f53f4c7 Abstract error sanitization 2025-10-23 18:47:11 +13:00
Jake Barnby 462a2d505d Remove unused scopes 2025-10-23 18:17:35 +13:00
Jake Barnby 654125a6a8 Review comments 2025-10-23 17:21:05 +13:00
Jake BarnbyGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
d4e5a1bdf2 Update src/Appwrite/Platform/Workers/Migrations.php
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-10-23 17:03:16 +13:00
Jake Barnby 26e07b8c26 Update specs 2025-10-23 16:58:29 +13:00
Jake Barnby 2a4f191a69 Fix log sets 2025-10-23 16:49:37 +13:00
Steven NguyenandGitHub aec26277c7 Merge pull request #10500 from appwrite/copilot/fix-3b5374b7-acb3-4c22-9573-00c1feec5bc1
Fix hardcoded specification defaults to use dynamic plan-based logic
2025-10-22 15:41:11 -07:00
Luke B. SilverandGitHub 887820dbec Merge pull request #10683 from appwrite/fix-stats-usage-memory-leak
fix: stats usage memory leak
2025-10-22 17:53:47 +01:00
loks0n 18371eb511 fix: stats usage memory leak 2025-10-22 17:53:16 +01:00
Matej BačoandGitHub c38d319aeb Merge pull request #10535 from appwrite/fix-author-url-in-template-deployments
Fix Author URL in template deployments
2025-10-22 17:35:41 +02:00
Khushboo Verma 9c55da5b99 Resolve merge conflicts 2025-10-22 20:44:47 +05:30
Matej Bačo 3019551db5 remove leftover todo 2025-10-22 16:04:30 +02:00
shimon 25403ea35e composer.lock 2025-10-22 16:43:49 +03:00
shimon 815c75fa9a Merge branch '1.8.x' of github.com:appwrite/appwrite into refactor-auth-single-instance
# Conflicts:
#	composer.lock
2025-10-22 16:42:11 +03:00
shimon 8b3b07a710 linter 2025-10-22 16:40:00 +03:00
shimon 33f88fa5e0 Refactor authorization handling across multiple modules to use the new getAuthorization() method. Update CHANGES.md to reflect the change in usage worker authorization. Adjust composer.json to specify the correct version for utopia-php/database. Update plugin-api-version in composer.lock. 2025-10-22 16:35:48 +03:00
Matej Bačo d787408e1d Implement package-based framework detection 2025-10-22 15:28:49 +02:00
Jake Barnby c86702430b Merge branch 'feat-csv-export' of github.com:appwrite/appwrite into feat-csv-export 2025-10-23 02:23:15 +13:00
Jake Barnby 597dc308a6 Fix test 2025-10-23 02:22:49 +13:00
Jake BarnbyandGitHub f89dc6c1ae Merge branch '1.8.x' into feat-csv-export 2025-10-22 12:48:39 +00:00
Jake Barnby 95902fa897 Fix lock 2025-10-23 01:39:25 +13:00
Jake Barnby d2aa971ed3 Update lock 2025-10-23 01:33:28 +13:00
Jake Barnby 9a1b5d4eee Only parse for validation 2025-10-23 01:28:06 +13:00
Matej Bačo a5486128d4 Update composer.lock 2025-10-22 13:41:55 +02:00
Matej Bačo 14b1cc6e48 Merge branch '1.8.x' into feat-tanstack-start-sites 2025-10-22 13:41:37 +02:00
Matej Bačo f9257c1ec9 add tanstack template, use detection lib v 0.2 2025-10-22 13:40:49 +02:00
Luke B. SilverandGitHub 45e938b4ba Merge pull request #10679 from appwrite/update-apple-swift-13.3.0
chore: update apple sdk to 13.3.0
2025-10-22 12:17:33 +01:00
Jake Barnby c177d54f59 Update lock 2025-10-22 23:51:28 +13:00
Jake Barnby de7f69ae3f Remove invalid param 2025-10-22 23:49:11 +13:00
Jake Barnby 7249083152 Pass through queries 2025-10-22 23:23:07 +13:00
Eldad Fux e00a73f442 Enhance avatar screenshot API by adding 'scale' parameter for browser scaling; update related tests to validate new functionality and edge cases. 2025-10-22 10:22:17 +01:00
Jake Barnby d5bd6a0475 Fix file ID 2025-10-22 22:06:55 +13:00
Chirag Aggarwal c2d4997810 update apple to 13.3.0 2025-10-22 14:17:57 +05:30
Jake Barnby c9d83cb3f5 Custom writer 2025-10-22 20:37:03 +13:00
Matej BačoandGitHub de03506c0a Merge pull request #10674 from appwrite/feat-SER-457-add-max-build-duration-env
feat: add _APP_COMPUTE_BUILD_TIMEOUT to console variables
2025-10-22 09:32:46 +02:00
Harsh MahajanandGitHub b6349b17f2 Merge branch '1.8.x' into feat-SER-457-add-max-build-duration-env 2025-10-22 10:24:37 +05:30
Jake BarnbyandGitHub 2e699ef8b9 Merge pull request #10678 from appwrite/update-domains-0.9.1
update domain lib to 0.9.1
2025-10-22 04:53:35 +00:00
Harsh MahajanandGitHub 32dae9ad7c Update Comment.php 2025-10-22 10:05:18 +05:30
Harsh MahajanandGitHub f5034a6ad8 Merge branch '1.8.x' into feat-SER-457-add-max-build-duration-env 2025-10-22 09:56:56 +05:30
Harsh MahajanandGitHub be5c5f9420 Merge branch 'main' into feat-SER-193-Update-facts-on-github-sites-and-functions 2025-10-22 09:53:45 +05:30
Jake Barnby 929baa17db Fix capture 2025-10-22 17:18:57 +13:00
Jake Barnby 8ded7c5942 Add options on response 2025-10-22 17:15:06 +13:00
Chirag Aggarwal 9c30f32281 update domain lib to 0.9.1 2025-10-22 09:29:45 +05:30
Jake Barnby 94c309d61e Fix default escape 2025-10-22 16:41:00 +13:00
Jake BarnbyandGitHub 72c608bf3b Merge pull request #10677 from appwrite/chore-sync-1.8.0
Chore sync 1.8.0 into main
2025-10-22 01:09:41 +00:00
Eldad Fux dfe87a0d37 Rename avatar screenshot endpoint from '/v1/avatars/screenshot' to '/v1/avatars/screenshots' for consistency; update related tests accordingly. 2025-10-22 01:40:31 +01:00
Eldad Fux bb77319703 Update browser endpoint in avatar screenshot API to use service name for improved reliability 2025-10-22 01:30:04 +01:00
Eldad Fux dc7bb627d5 Enhance avatar screenshot API with new parameters and validations; add GraphQL support and extensive tests for various scenarios 2025-10-22 01:24:31 +01:00
Steven NguyenandGitHub 2652a21332 Merge pull request #10404 from appwrite/feat-1.8.0-release-prep
Prepare 1.8.0 release
2025-10-21 15:40:07 -07:00
Eldad Fux f35b80ba1a Refactor avatar screenshot handling and add comprehensive tests for various header and parameter validations 2025-10-21 21:20:31 +01:00
Eldad Fux 6cc5d1595d Fixes 2025-10-21 21:03:13 +01:00
Steven Nguyen 60b4835539 Merge branch 'feat-1.8.0-release-prep' into chore-sync-1.8.0 2025-10-21 12:24:26 -07:00
Eldad Fux 5073a86581 POC - website screenshots 2025-10-21 15:37:05 +01:00
Jake Barnby 4619e03d64 Update specs 2025-10-22 01:26:01 +13:00
Harsh MahajanandGitHub d48ef373a7 Fix environment variable assignment for build timeout 2025-10-21 17:48:48 +05:30
Harsh MahajanGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
4cb20b3fe3 Update app/controllers/api/console.php
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-10-21 17:47:30 +05:30
Jake Barnby f1c68af091 Add queries for csv export 2025-10-22 01:12:18 +13:00
Harsh Mahajan 803244ccf7 feat:add _APP_COMPUTE_BUILD_TIMEOUT to console variables 2025-10-21 11:48:48 +00:00
Jake Barnby 215d55a997 Merge remote-tracking branch 'origin/1.8.x' into feat-csv-export
# Conflicts:
#	composer.json
#	composer.lock
2025-10-22 00:34:01 +13:00
Luke B. SilverandGitHub 16a2dd0b27 Merge pull request #10672 from appwrite/update-cli-10.2.2
chore: update cli to 10.2.2
2025-10-21 10:53:16 +01:00
shimon ab1e510348 Enhance API response handling by adding 'includeTotal' parameter to account endpoints, allowing control over total count calculation for sessions, logs, and identities. Updated tests to verify functionality. 2025-10-21 11:41:41 +03:00
shimon 232f1b1c05 account sync 2025-10-21 11:24:09 +03:00
shimon 1606141a5a Merge branch '1.8.x' of github.com:appwrite/appwrite into feat-disable-count
# Conflicts:
#	app/controllers/api/account.php
2025-10-21 11:14:05 +03:00
shimon c8f8b6ff0c account sync 2025-10-21 10:54:03 +03:00
shimon 095ee8511b composer 2025-10-21 10:44:17 +03:00
shimon bb0889fdba test fix 2025-10-21 10:35:01 +03:00
Chirag Aggarwal 8154a8524a update sdk gen 2025-10-21 12:51:10 +05:30
Chirag Aggarwal 0e567e3e68 chore: update cli to 10.2.2 2025-10-21 11:40:50 +05:30
Steven Nguyen 610a359160 chore: clean up CHANGES.md
1. Remove PR authors because they don't render properly anyways
2. Format PR links to use markdown style links instead of plain URLs
2025-10-20 21:15:38 -07:00
Steven Nguyen a3f51298fe chore: update CHANGES.md for 1.8.0 release 2025-10-20 21:15:38 -07:00
Steven Nguyen 46f249fd3b feat: bump console to version 7.4.7 2025-10-20 21:15:38 -07:00
Steven Nguyen 97312d1a6d feat(self-hosted): update migration for transactions collections 2025-10-20 21:15:38 -07:00
Steven Nguyen 01765fd27f feat: bump console to version 7.4.4 2025-10-20 21:15:37 -07:00
Steven Nguyen 0a809f85f2 fix(self-hosted): create missing project attributes
Appwrite 1.6.1 added these attributes, but the migration was never updated to include those new attributes
2025-10-20 21:15:37 -07:00
Steven Nguyen a9b18811ea fix(self-hosted): clear cache for collections and documents
In older versions of Appwrite, the internal ID was the $internalId attribute. However, it has now changed to $sequence so that it can align with the publicly exposed attribute for an auto-incrementing ID.

The problem with this change is that data in the cache still references the old $internalId attribute, which can lead to inconsistencies and errors when accessing cached documents. To resolve this issue, we need to clear the cache for all collections and documents at the beginning of the migration so that the new $sequence attribute is used.
2025-10-20 21:15:37 -07:00
Steven Nguyen 59f82a0dd7 feat: bump console to version 7.1.11 2025-10-20 21:15:37 -07:00
Steven Nguyen 00615c2f38 chore: create migration version for 1.8.x 2025-10-20 21:15:37 -07:00
Jake BarnbyandGitHub f150cffa17 Merge pull request #10670 from appwrite/revert-auth
Revert "Merge pull request #10468 from appwrite/feat-apps-module-dl"
2025-10-21 02:58:23 +00:00
Jake Barnby f4efb81832 Update lock 2025-10-21 15:07:53 +13:00
Jake Barnby ddde13a78f Revert "Merge pull request #10468 from appwrite/feat-apps-module-dl"
This reverts commit 9dd1939d1f, reversing
changes made to 8dfdfcb522.

# Conflicts:
#	app/config/collections/common.php
#	app/controllers/api/users.php
#	app/init/resources.php
#	composer.lock
2025-10-21 14:59:30 +13:00
shimon a643cde26d Update 'includeTotal' parameter to use Boolean(true) for consistent behavior across API endpoints, enhancing response handling for sessions, logs, memberships, and more. 2025-10-20 22:00:57 +03:00
shimon 5f3316ff18 fix assertion order 2025-10-20 18:38:14 +03:00
shimon 9be7f4d72c Merge remote-tracking branch 'origin/feat-disable-count' into feat-disable-count 2025-10-20 18:28:14 +03:00
shimon 76f4483dda Merge branch '1.8.x' of github.com:appwrite/appwrite into feat-disable-count 2025-10-20 18:27:36 +03:00
shimon fb4e3e1b9d Implement 'includeTotal' parameter across multiple API endpoints to control total count in responses, ensuring consistent behavior in session, log, and membership retrievals. 2025-10-20 18:18:17 +03:00
Jake BarnbyandGitHub e2d3624713 Merge pull request #10664 from appwrite/feat-update-db
Update db
2025-10-20 14:11:36 +00:00
shimon 55a33c135c Merge branch '1.8.x' of github.com:appwrite/appwrite into feat-disable-count 2025-10-20 17:11:32 +03:00
Matej BačoandGitHub 411951f9d5 Merge pull request #10669 from appwrite/copilot/update-github-comment-title
Change GitHub alert from NOTE to TIP in VCS build comments
2025-10-20 16:03:59 +02:00
Jake Barnby caa699e181 Update database 2025-10-21 02:25:51 +13:00
Jake Barnby 94d02aff4f Fix DB tests 2025-10-21 01:24:50 +13:00
Jake Barnby d23f993a6c Fix validator constructor 2025-10-21 00:13:43 +13:00
Jake Barnby bfe639fb70 Fix key 2025-10-21 00:13:32 +13:00
Jake Barnby 568651291e Update libs 2025-10-20 23:13:17 +13:00
shimon 09075a05db typo fix 2025-10-19 16:22:13 +03:00
shimon 9f80952251 linter 2025-10-19 16:15:13 +03:00
shimon d0e0f67fee Implement 'includeTotal' parameter across various API endpoints to control total count in responses 2025-10-19 15:45:16 +03:00
shimon f16de4abb9 Refactor XList classes to consistently implement 'includeTotal' parameter for total count control in database queries 2025-10-19 14:46:55 +03:00
shimon 7bbe08f23e Enhance XList classes to support 'includeTotal' parameter for controlling total count in database queries 2025-10-19 14:38:32 +03:00
shimon 3229956eef Enhance XList actions to include 'includeTotal' parameter for total count control 2025-10-19 13:16:39 +03:00
Jake Barnby 11042af051 Update db 2025-10-18 02:19:43 +13:00
Hemachandar 635d77350a set header 2025-10-15 16:05:34 +05:30
Hemachandar 9f07a2bca4 early return 2025-10-15 15:16:28 +05:30
Steven Nguyen 56a15efe3f chore: bump appwrite version to 1.8.0 2025-10-14 14:42:59 -07:00
Hemachandar bad74bd0b8 Add cache header for deployment download API 2025-10-14 18:18:41 +05:30
Hemachandar c525e15ea5 Dark mode styles for emails 2025-10-10 23:01:26 +05:30
Khushboo VermaandGitHub 0c64eaf441 Merge branch '1.8.x' into copilot/fix-3b5374b7-acb3-4c22-9573-00c1feec5bc1 2025-10-07 19:56:12 +05:30
Khushboo VermaandGitHub 248e538518 Merge branch '1.8.x' into copilot/fix-3b5374b7-acb3-4c22-9573-00c1feec5bc1 2025-10-07 16:56:48 +05:30
Khushboo VermaandGitHub 1a07123abe Merge branch '1.8.x' into copilot/fix-3b5374b7-acb3-4c22-9573-00c1feec5bc1 2025-10-07 14:14:16 +05:30
Khushboo VermaandGitHub 35578bfada Merge branch '1.8.x' into copilot/fix-3b5374b7-acb3-4c22-9573-00c1feec5bc1 2025-10-07 12:10:42 +05:30
Harsh MahajanGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
64c3b7a2ff Update src/Appwrite/Vcs/Comment.php
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-10-02 23:19:34 +05:30
Harsh Mahajan 61a8f589ac port issue 2025-10-02 17:44:21 +00:00
Harsh Mahajan 92ccb6e4ea feat(vcs): expand GitHub comment tips 2025-10-02 17:43:30 +00:00
Priyanshu ThapliyalandGitHub cc6f5fea6d Merge branch '1.8.x' into fix/sitesCreateDeployment 2025-10-01 22:21:58 +05:30
Khushboo VermaandGitHub bd772c97cf Merge branch '1.8.x' into copilot/fix-3b5374b7-acb3-4c22-9573-00c1feec5bc1 2025-10-01 12:39:14 +01:00
Priyanshu Thapliyal 12e3af6ad7 add specs files 2025-10-01 08:44:14 +00:00
Priyanshu Thapliyal f0b6eeb303 fix: sites deployment activation instructions in createDeployment 2025-10-01 08:31:42 +00:00
Khushboo VermaandGitHub b40769b086 Merge branch '1.8.x' into copilot/fix-3b5374b7-acb3-4c22-9573-00c1feec5bc1 2025-09-29 22:48:38 +01:00
Jake Barnby 16821a28fc Format 2025-09-24 22:53:13 +12:00
Jake Barnby 5572c4a375 Merge remote-tracking branch 'origin/1.8.x' into feat-csv-export
# Conflicts:
#	composer.lock
2025-09-24 22:40:04 +12:00
Jake Barnby 2494c9afb3 Update to release migrations 2025-09-24 22:39:06 +12:00
Jake Barnby 223523f722 Mount uploads to migrations worker 2025-09-24 22:38:55 +12:00
Jake Barnby 61d9db8c67 Cover export + notify in tests 2025-09-24 22:38:35 +12:00
Jake Barnby 4970aa7426 Send email if notify on complete is set 2025-09-24 22:38:12 +12:00
Jake Barnby 5e8951fbe0 Save export to bucket on complete 2025-09-24 22:37:49 +12:00
Jake Barnby c8993d7f71 Add additional export params 2025-09-24 22:37:12 +12:00
Atharva Deosthale 5963b2baf7 change request filter to versions below 1.9.0 2025-09-23 18:00:14 +05:30
Jake Barnby 97c5705447 Merge remote-tracking branch 'origin/1.8.x' into feat-csv-export 2025-09-23 19:17:49 +12:00
Khushboo Verma 5d5c1d8f43 Fix Author URL in template deployments 2025-09-22 20:29:25 +05:30
Jake Barnby a0afce666f Merge remote-tracking branch 'origin/1.8.x' into feat-csv-export
# Conflicts:
#	composer.lock
2025-09-22 16:05:58 +12:00
Atharva Deosthale 7d4486b9e6 more fixes to tests 2025-09-19 18:42:25 +05:30
Atharva Deosthale 1a19e01d69 attempt to fix tests 2025-09-19 18:30:42 +05:30
Atharva Deosthale 2a145bc284 address reviews regarding tests 2025-09-19 18:18:44 +05:30
Atharva Deosthale 9fe3e94a66 e2e tests 2025-09-18 13:12:46 +05:30
Atharva Deosthale 8e32fb05d1 specs 2025-09-18 12:28:08 +05:30
Atharva Deosthale de8538b622 use request filters for 1.8.0 to convert into and 2025-09-18 12:27:24 +05:30
Steven NguyenandGitHub 536cff9cc6 chore: add specification validator test 2025-09-15 21:39:23 +00:00
Steven NguyenandGitHub 62014fda26 fix: implement dynamic specification defaults for Functions and Sites
Prior to this, self-hosted instances would defualt to the lowest specification.

Now, if there is no plan (ie. on self-hosted), the highest specification is used.

If there is a plan, the lowest specification available in the plan is used.
2025-09-15 21:39:22 +00:00
copilot-swe-agent[bot]andstnguyen90 fea3544d4b Implement dynamic specification defaults for Functions and Sites
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-09-15 19:18:07 +00:00
copilot-swe-agent[bot] 679de3574f Initial plan 2025-09-15 19:11:33 +00:00
Jake Barnby 981a1278e2 Merge remote-tracking branch 'origin/1.8.x' into feat-csv-export
# Conflicts:
#	composer.lock
2025-09-15 16:30:22 +12:00
Atharva Deosthale fca2727966 add validation for type 2025-09-14 15:28:25 +05:30
Atharva Deosthale cb03bfe74c specs and sdk 2025-09-14 15:22:51 +05:30
Atharva Deosthale 2b1e3bd947 delete the new route and just do it in template route 2025-09-14 15:15:44 +05:30
Atharva Deosthale 4858317891 fix: format, lints 2025-09-13 18:02:13 +05:30
Atharva Deosthale 6e16340f16 feat: add branch deployments to appwrite 2025-09-13 17:56:19 +05:30
ArnabChatterjee20k ac604b11e3 added migration script 2025-09-01 13:13:52 +05:30
ArnabChatterjee20k f68d49c4d6 updated the migration error size attribute 2025-08-29 18:19:29 +05:30
Jake Barnby 1722e9e416 Fix DB read source 2025-08-22 18:03:10 +12:00
Jake Barnby 57a46b98ec Use utopia adapter 2025-08-22 18:01:38 +12:00
Jake Barnby 74c0f6a140 Merge remote-tracking branch 'origin/1.8.x' into feat-csv-export
# Conflicts:
#	app/controllers/api/migrations.php
#	composer.lock
2025-08-22 01:41:33 +12:00
Jake Barnby 892cfe01b2 Optional columns 2025-08-07 00:47:11 +12:00
Jake Barnby bf1af094c1 Add specific column selection 2025-08-06 00:40:39 +12:00
Jake Barnby 2f5dc7782f Merge remote-tracking branch 'origin/1.7.x' into feat-csv-export 2025-08-06 00:20:02 +12:00
Jake Barnby c70915306d WIP export 2025-08-05 17:04:31 +12:00
1078 changed files with 39385 additions and 8412 deletions
+2
View File
@@ -5,3 +5,5 @@ src/** linguist-detectable=false
tests/** linguist-detectable=false
public/scripts/** linguist-detectable=false
public/dist/scripts/** linguist-detectable=false
.github/workflows/*.lock.yml linguist-generated=true merge=ours
+83
View File
@@ -0,0 +1,83 @@
# Fixes and upgrades for the Appwrite Auth / Users / Teams services.
"product / auth":
- "(auth|session|login|logout|register|2fa|mfa|users|teams|memberships|invite|oauth|oauth2|sso|jwt)"
# Fixes and upgrades for the Appwrite Realtime API.
"api / realtime":
- "(realtime|subscribe|websockets)"
# Console, UI and UX issues
"product / console":
- "(console)"
# Fixes and upgrades for the Appwrite Storage.
"product / storage":
- "(storage|bucket|file|image|preview|download)"
# Fixes and upgrades for the Appwrite Database.
"product / databases":
- "(database|collection|tables|attribute|column|document|row|query|queries|indexes|search|filter|sort|pagination)"
# Fixes and upgrades for the Appwrite Functions.
"product / functions":
- "(function|runtime|deployment|execution|trigger|cron|schedule)"
# Fixes and upgrades for the Appwrite Docs.
# "product / docs":
# -
# Fixes and upgrades for the Appwrite Migrations.
"product / migrations":
- "(migrate|migration)"
# Fixes and upgrades for the Appwrite Messaging.
"product / messaging":
- "(messaging|email|sms|push|provider|topic|target|notification)"
# Fixes and upgrades for the Appwrite Platform.
# "product / platform":
# -
# Fixes and upgrades for database relationships
"feature / relationships":
- "(relationship)"
# Issues found only on Appwrite Cloud
# "product / cloud":
# -
# Fixes and upgrades for the Appwrite VCS.
"product / vcs":
- "(repo|push|vcs|repository)"
# Fixes and upgrades for the Appwrite GraphQL API.
"api / graphql":
- "(graphql|gql|mutation)"
# Fixes and upgrades for the Appwrite Assistant.
"product / assistant":
- "(assistant)"
# Fixes and upgrades for the Appwrite Domains.
"product / domains":
- "(domain|dns|ssl|certificate)"
# Fixes and upgrades for the Appwrite Locale.
"product / locale":
- "(locale|i18n|internationalization|localization|l10n|translation|timezone|country)"
# Fixes and upgrades for the Appwrite Avatars.
"product / avatars":
- "(avatar|initial|flag|icon)"
# Fixes and upgrades for Appwrite Sites.
"product / sites":
- "(site|web|hosting|domain|ssl|certificate|nextjs|nuxt|react|angular|vue|svelte|astro)"
# Fixes and upgrades for the Appwrite CLI.
"sdk / cli":
- "(cli|command line)"
# Issues only found when self-hosting Appwrite
"product / self-hosted":
- "(self-host|self host)"
+32
View File
@@ -0,0 +1,32 @@
name: AI Moderator
on:
issues:
types: [opened, edited]
issue_comment:
types: [created, edited]
pull_request:
types: [opened, edited]
pull_request_review:
types: [submitted, edited]
pull_request_review_comment:
types: [created, edited]
discussion:
types: [created, edited]
discussion_comment:
types: [created, edited]
permissions:
models: read
issues: write
pull-requests: write
discussions: write
jobs:
moderate:
runs-on: ubuntu-latest
steps:
- name: AI Moderator
uses: github/ai-moderator@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
+22
View File
@@ -0,0 +1,22 @@
name: Auto Label Issue
on:
issues:
types: [opened]
permissions:
issues: write
contents: read
jobs:
labeler:
runs-on: ubuntu-latest
steps:
- name: Issue Labeler
uses: github/issue-labeler@v3.4
with:
configuration-path: .github/labeler.yml
enable-versioned-regex: false
include-title: 1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
File diff suppressed because it is too large Load Diff
+85
View File
@@ -0,0 +1,85 @@
---
on:
schedule:
- cron: '0 0 * * *' # Run daily at midnight UTC
workflow_dispatch: # Enable manual trigger
stop-after: +30d # workflow will no longer trigger after 30 days. Remove this and recompile to run indefinitely
reaction: eyes
permissions: read-all
network: defaults
safe-outputs:
add-labels:
max: 5
add-comment:
tools:
web-fetch:
web-search:
timeout_minutes: 10
source: githubnext/agentics/workflows/issue-triage.md@0837fb7b24c3b84ee77fb7c8cfa8735c48be347a
---
# Agentic Triage
<!-- Note - this file can be customized to your needs. Replace this section directly, or add further instructions here. After editing run 'gh aw compile' -->
You're a triage assistant for GitHub issues. Your task is to analyze issues created in the last 24 hours and perform initial triage tasks for each of them.
1. First, use the `list_issues` tool to retrieve all issues created in the last 24 hours. Filter issues by using the `since` parameter with a timestamp from 24 hours ago (calculate: current time minus 24 hours in ISO 8601 format).
2. For each issue found, perform the following triage tasks:
3. Select appropriate labels for the issue from the provided list.
4. Retrieve the issue content using the `get_issue` tool. If the issue is obviously spam, or generated by bot, or something else that is not an actual issue to be worked on, then add an issue comment to the issue with a one sentence analysis and move to the next issue.
5. Next, use the GitHub tools to gather additional context about the issue:
- Fetch the list of labels available in this repository. Use 'gh label list' bash command to fetch the labels. This will give you the labels you can use for triaging issues.
- Fetch any comments on the issue using the `get_issue_comments` tool
- **Search for duplicate and related issues**: Use the `search_issues` tool to find similar issues by searching for key terms from the issue title and description. Look for both open and closed issues that might be related or duplicates.
6. Analyze the issue content, considering:
- The issue title and description
- The type of issue (bug report, feature request, question, etc.)
- Technical areas mentioned
- Severity or priority indicators
- User impact
- Components affected
7. Write notes, ideas, nudges, resource links, debugging strategies and/or reproduction steps for the team to consider relevant to the issue.
8. Select appropriate labels from the available labels list provided above:
- Choose labels that accurately reflect the issue's nature
- Be specific but comprehensive
- Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority)
- Consider platform labels (android, ios) if applicable
- Search for similar issues, and if you find similar issues consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
- Only select labels from the provided list above
- It's okay to not add any labels if none are clearly applicable
9. Apply the selected labels:
- Use the `update_issue` tool to apply the labels to the issue
- DO NOT communicate directly with users
- If no labels are clearly applicable, do not apply any labels
10. Add an issue comment to the issue with your analysis:
- Start with "🎯 Agentic Issue Triage"
- Provide a brief summary of the issue
- **If duplicate or related issues were found**, add a section listing them with links (e.g., "### 🔗 Potentially Related Issues" followed by a bullet list of related issues with their titles and links)
- Mention any relevant details that might help the team understand the issue better
- Include any debugging strategies or reproduction steps if applicable
- Suggest resources or links that might be helpful for resolving the issue or learning skills related to the issue or the particular area of the codebase affected by it
- Mention any nudges or ideas that could help the team in addressing the issue
- If you have possible reproduction steps, include them in the comment
- If you have any debugging strategies, include them in the comment
- If appropriate break the issue down to sub-tasks and write a checklist of things to do.
- Use collapsed-by-default sections in the GitHub markdown to keep the comment tidy. Collapse all sections except the short main summary at the top.
11. After processing all issues, provide a summary of how many issues were triaged. If no issues were created in the last 24 hours, simply note that no new issues needed triage.
+639 -251
View File
@@ -1,266 +1,654 @@
# Version 1.8.0
## What's Changed
### Notable changes
* Do not allow full range in [#9847](https://github.com/appwrite/appwrite/pull/9847)
* Expose internal id as a part of auto increment id in [#9713](https://github.com/appwrite/appwrite/pull/9713)
* Expose sequence in [#9870](https://github.com/appwrite/appwrite/pull/9870)
* Add flutter 3.32 and dart 3.8 runtimes in [#9914](https://github.com/appwrite/appwrite/pull/9914)
* Shorten commit url and branch url in [#9919](https://github.com/appwrite/appwrite/pull/9919)
* Remove powered by from error pages in [#9927](https://github.com/appwrite/appwrite/pull/9927)
* Enable resource limits on GIF previews in [#9940](https://github.com/appwrite/appwrite/pull/9940)
* Only run maintenance task for projects accessed in last 24 hours in [#9989](https://github.com/appwrite/appwrite/pull/9989)
* Add increment + decrement routes in [#9986](https://github.com/appwrite/appwrite/pull/9986)
* Only run maintenance task for projects accessed in last 30 days in [#9995](https://github.com/appwrite/appwrite/pull/9995)
* Update appwrite-assistant image version to 0.8.3 in [#10003](https://github.com/appwrite/appwrite/pull/10003)
* Update emails to use button in [#9590](https://github.com/appwrite/appwrite/pull/9590)
* Create commit & branch url for first git deployment when site is linked to repo in [#9969](https://github.com/appwrite/appwrite/pull/9969)
* Handle React Native schemes in [#9650](https://github.com/appwrite/appwrite/pull/9650)
* Handle origin validation for web extensions in [#10107](https://github.com/appwrite/appwrite/pull/10107)
* Preview text for emails in [#10198](https://github.com/appwrite/appwrite/pull/10198)
* Create email target when using email OTP registration in [#10224](https://github.com/appwrite/appwrite/pull/10224)
* Add CSV imports in [#10231](https://github.com/appwrite/appwrite/pull/10231)
* Add support for svg favicons in [#10255](https://github.com/appwrite/appwrite/pull/10255)
* Realtime support for bulk api in [#10096](https://github.com/appwrite/appwrite/pull/10096)
* Skip redundant subqueries in users list route in [#10297](https://github.com/appwrite/appwrite/pull/10297)
* Add native sign in with Apple function template in [#10286](https://github.com/appwrite/appwrite/pull/10286)
* Add support for HEAD requests in [#10304](https://github.com/appwrite/appwrite/pull/10304)
* Update invite email copy in [#10309](https://github.com/appwrite/appwrite/pull/10309)
* Increase dynamic API key expiration in [#10328](https://github.com/appwrite/appwrite/pull/10328)
* Add TablesDB service in [#10333](https://github.com/appwrite/appwrite/pull/10333)
* Add execution.deploymentId to response model in [#10357](https://github.com/appwrite/appwrite/pull/10357)
* Switch Union China Pay to just Union Pay in [#10372](https://github.com/appwrite/appwrite/pull/10372) and [#10382](https://github.com/appwrite/appwrite/pull/10382)
* Add execution id and log id to response headers in [#10379](https://github.com/appwrite/appwrite/pull/10379)
* Add executionId and client IP to function headers in [#9147](https://github.com/appwrite/appwrite/pull/9147)
* Allow HEAD requests in function executions in [#10385](https://github.com/appwrite/appwrite/pull/10385)
* Add support for select queries when listing deployments in [#10380](https://github.com/appwrite/appwrite/pull/10380)
* Add spatial type attributes in [#10356](https://github.com/appwrite/appwrite/pull/10356) and [#10443](https://github.com/appwrite/appwrite/pull/10443)
* Add realtime support for bulk upserts in [#10425](https://github.com/appwrite/appwrite/pull/10425)
* Add previewUrl to vcs comment from vcs controller in [#10396](https://github.com/appwrite/appwrite/pull/10396)
* Rename verification SDK methods to be more specific in [#10606](https://github.com/appwrite/appwrite/pull/10606)
* Add project name in email subject in [#10609](https://github.com/appwrite/appwrite/pull/10609)
* Throw error when email is not available for account verification in [#10533](https://github.com/appwrite/appwrite/pull/10533)
* Add support for transactions in [#10023](https://github.com/appwrite/appwrite/pull/10023) and [#10624](https://github.com/appwrite/appwrite/pull/10624)
* Use bcc only emails for smtp in [#10644](https://github.com/appwrite/appwrite/pull/10644)
### Fixes
* Fix rules on active deployment in [#9902](https://github.com/appwrite/appwrite/pull/9902)
* Fix for upserts with differing optional parameter sets in [#9928](https://github.com/appwrite/appwrite/pull/9928)
* Fix teams deletion in [#9888](https://github.com/appwrite/appwrite/pull/9888)
* Fix deletion logic in [#9938](https://github.com/appwrite/appwrite/pull/9938)
* Update database for upsert fix in [#9941](https://github.com/appwrite/appwrite/pull/9941)
* Fix expire format in account recovery, verification, phone and mfa in [#9600](https://github.com/appwrite/appwrite/pull/9600)
* Fix github comments and deployment creation on branch deletion in [#9949](https://github.com/appwrite/appwrite/pull/9949)
* Fix cache issues with proxy for deployment download in [#9971](https://github.com/appwrite/appwrite/pull/9971)
* Redirect rule parent resource in [#9982](https://github.com/appwrite/appwrite/pull/9982)
* Fix usage queues in [#9946](https://github.com/appwrite/appwrite/pull/9946)
* Transfer control for the migration in [#9997](https://github.com/appwrite/appwrite/pull/9997)
* Prevent 'Attribute "factors" must be an array' error in [#10004](https://github.com/appwrite/appwrite/pull/10004)
* Fix all vcs urls missing region in [#9998](https://github.com/appwrite/appwrite/pull/9998)
* Add readable error for csv imports in [#9947](https://github.com/appwrite/appwrite/pull/9947)
* Fix missing screenshot logs in [#10024](https://github.com/appwrite/appwrite/pull/10024)
* Update executor to fix s3 endpoint bug in [#10036](https://github.com/appwrite/appwrite/pull/10036)
* Fix build duration calculation in [#10053](https://github.com/appwrite/appwrite/pull/10053)
* Fix logs order in [#10052](https://github.com/appwrite/appwrite/pull/10052)
* Fix platform check for Sites with automatic rule in [#10043](https://github.com/appwrite/appwrite/pull/10043)
* Increase cache ttl to ensure hits in [#10079](https://github.com/appwrite/appwrite/pull/10079)
* Fix connect to existing repo flow in [#10034](https://github.com/appwrite/appwrite/pull/10034)
* Fix migrations path and type in [#10090](https://github.com/appwrite/appwrite/pull/10090)
* Fix JWT authentication database selection for admin mode in [#10098](https://github.com/appwrite/appwrite/pull/10098)
* Use _APP_CONSOLE_DOMAIN, if not found, then use _APP_DOMAIN in [#9999](https://github.com/appwrite/appwrite/pull/9999)
* Fix file tokens not working on file-security in [#10120](https://github.com/appwrite/appwrite/pull/10120)
* Fix build activation race condition in [#9952](https://github.com/appwrite/appwrite/pull/9952)
* Changed the default permission param of upsert document in [#10129](https://github.com/appwrite/appwrite/pull/10129)
* Fix success validation in oauth2 redirect in [#10130](https://github.com/appwrite/appwrite/pull/10130)
* Update OAuth2 redirect URLs in [#10119](https://github.com/appwrite/appwrite/pull/10119)
* Fix specs with new env vars in [#10135](https://github.com/appwrite/appwrite/pull/10135)
* Skip deployment when commit is created by us in [#10187](https://github.com/appwrite/appwrite/pull/10187)
* Use direct source for file-preview when empty in [#10181](https://github.com/appwrite/appwrite/pull/10181)
* Better error message for invalid function scheduled time in [#10201](https://github.com/appwrite/appwrite/pull/10201)
* Add defaultBranch in getRepository response in [#10190](https://github.com/appwrite/appwrite/pull/10190)
* Filter sequence to int because any models skip rule checks in [#10221](https://github.com/appwrite/appwrite/pull/10221)
* Fix 500 errors on robots and humans txt files in [#10248](https://github.com/appwrite/appwrite/pull/10248)
* Fix atomic number ops with limit 0 in [#10264](https://github.com/appwrite/appwrite/pull/10264)
* Update build command for flutter in [#10288](https://github.com/appwrite/appwrite/pull/10288)
* Add a fallback locale in [#10307](https://github.com/appwrite/appwrite/pull/10307)
* Fix variables sharing across resources in [#10308](https://github.com/appwrite/appwrite/pull/10308)
* Fix uncaught invalid arg in [#10318](https://github.com/appwrite/appwrite/pull/10318)
* Add missing upsert event in [#10317](https://github.com/appwrite/appwrite/pull/10317)
* Improve font reliability in [#10332](https://github.com/appwrite/appwrite/pull/10332)
* Truncate logs in function worker in [#9773](https://github.com/appwrite/appwrite/pull/9773)
* Fix event template configuration issues in [#10350](https://github.com/appwrite/appwrite/pull/10350)
* Fix users events & missed publisher logic for Functions in [#10348](https://github.com/appwrite/appwrite/pull/10348)
* Fix incorrect file token expiry in [#10329](https://github.com/appwrite/appwrite/pull/10329)
* Fix upserting that makes no change in [#10363](https://github.com/appwrite/appwrite/pull/10363) and [#10364](https://github.com/appwrite/appwrite/pull/10364)
* Fix domain validator in [#10374](https://github.com/appwrite/appwrite/pull/10374)
* Apply sequence integer casting and attribute cleanup fixes to Row model, TablesDB tests, and document processing in [#10383](https://github.com/appwrite/appwrite/pull/10383)
* Fix domain validator in [#10386](https://github.com/appwrite/appwrite/pull/10386)
* Fix sequence removal in [#10388](https://github.com/appwrite/appwrite/pull/10388)
* Fix TablesDB scopes in [#10387](https://github.com/appwrite/appwrite/pull/10387)
* Fix request filter in [#10389](https://github.com/appwrite/appwrite/pull/10389)
* Fix nested filter selects in [#10393](https://github.com/appwrite/appwrite/pull/10393)
* Fix readonly attr stripping on write in [#10405](https://github.com/appwrite/appwrite/pull/10405)
* Replace %s with mustache placeholder in [#10392](https://github.com/appwrite/appwrite/pull/10392)
* Support array headers for set-cookie in [#10427](https://github.com/appwrite/appwrite/pull/10427)
* Fix put prefs structure validation in [#10436](https://github.com/appwrite/appwrite/pull/10436)
* Fix oauth identity check in [#10460](https://github.com/appwrite/appwrite/pull/10460)
* Fix check in [#10489](https://github.com/appwrite/appwrite/pull/10489)
* Fix database usage metrics in [#10483](https://github.com/appwrite/appwrite/pull/10483)
* Throw appropriate 400s from request filters in [#10502](https://github.com/appwrite/appwrite/pull/10502)
* Catch query exception on bucket/file list in [#10505](https://github.com/appwrite/appwrite/pull/10505)
* Use outputDirectory attribute from deployment in [#10571](https://github.com/appwrite/appwrite/pull/10571)
* Fix buildOutput attribute name in deployment check in [#10572](https://github.com/appwrite/appwrite/pull/10572)
* Update database for nested selection fix in [#10577](https://github.com/appwrite/appwrite/pull/10577)
* Auto-allow sites domain for OAuth in [#10503](https://github.com/appwrite/appwrite/pull/10503)
* Handle OIDC well-known endpoint errors in [#10589](https://github.com/appwrite/appwrite/pull/10589)
* Correct invalid template links in Create temporary deployment endpoint in [#10581](https://github.com/appwrite/appwrite/pull/10581)
* Update broken create table links in TablesDB docs in [#10592](https://github.com/appwrite/appwrite/pull/10592)
* Fix cross API compatibility in [#10626](https://github.com/appwrite/appwrite/pull/10626)
* Fix code 0 from databases on realtime in [#10631](https://github.com/appwrite/appwrite/pull/10631)
* Throw duplicate error when function id already exists in [#10618](https://github.com/appwrite/appwrite/pull/10618)
### Miscellaneous
* Fix task coroutine hooks in [#9850](https://github.com/appwrite/appwrite/pull/9850)
* Feat sync encrypt updates in [#9871](https://github.com/appwrite/appwrite/pull/9871)
* Revert "Feat sync encrypt updates" in [#9877](https://github.com/appwrite/appwrite/pull/9877)
* Add builds worker group in [#9872](https://github.com/appwrite/appwrite/pull/9872)
* Revert encrypted attribute changes in [#9898](https://github.com/appwrite/appwrite/pull/9898)
* Update sdk generator and sdks in [#9849](https://github.com/appwrite/appwrite/pull/9849)
* Release cli in [#9900](https://github.com/appwrite/appwrite/pull/9900)
* Improve how rules are fetched in [#9915](https://github.com/appwrite/appwrite/pull/9915)
* Sync 1.6 in [#9920](https://github.com/appwrite/appwrite/pull/9920)
* Update messaging library in [#9764](https://github.com/appwrite/appwrite/pull/9764)
* Disable TCP hook on stats resources in [#9932](https://github.com/appwrite/appwrite/pull/9932)
* Remove JSON index on roles due to MySQL bug in [#9924](https://github.com/appwrite/appwrite/pull/9924)
* Update queue in [#9936](https://github.com/appwrite/appwrite/pull/9936)
* Fix flaky account tests in [#9954](https://github.com/appwrite/appwrite/pull/9954)
* Fix flaky messaging test in [#9957](https://github.com/appwrite/appwrite/pull/9957)
* Make usage tests robust in [#9956](https://github.com/appwrite/appwrite/pull/9956)
* Increase deployment timeouts in tests in [#9955](https://github.com/appwrite/appwrite/pull/9955)
* Graceful shutdown on SIGTERM in [#9890](https://github.com/appwrite/appwrite/pull/9890)
* Bring back telemetry for storage in [#9903](https://github.com/appwrite/appwrite/pull/9903)
* Update version to 1.7.4 and add experimental warnings in [#9959](https://github.com/appwrite/appwrite/pull/9959)
* Return queue pre-fetch results in [#9731](https://github.com/appwrite/appwrite/pull/9731)
* Update SDK versions in [#9987](https://github.com/appwrite/appwrite/pull/9987)
* Restore unique filename for health check #9842 in [#9993](https://github.com/appwrite/appwrite/pull/9993)
* Add after build hook in [#9996](https://github.com/appwrite/appwrite/pull/9996)
* Remove endpoint selector in [#10000](https://github.com/appwrite/appwrite/pull/10000)
* Use static code instead of astro in tests in [#9966](https://github.com/appwrite/appwrite/pull/9966)
* Add ref param to vcs list contents in [#9991](https://github.com/appwrite/appwrite/pull/9991)
* Update coderabbit config file in [#10005](https://github.com/appwrite/appwrite/pull/10005)
* TAR support in [#10016](https://github.com/appwrite/appwrite/pull/10016)
* Update delete project scope in [#10017](https://github.com/appwrite/appwrite/pull/10017)
* Lazy-load relationships in [#9669](https://github.com/appwrite/appwrite/pull/9669)
* Revert "Feat: Lazy-load relationships" in [#10018](https://github.com/appwrite/appwrite/pull/10018)
* Revert "Update delete project scope" in [#10022](https://github.com/appwrite/appwrite/pull/10022)
* 1.8.x in [#9985](https://github.com/appwrite/appwrite/pull/9985)
* Update cli version and add bulk operation warnings in [#10007](https://github.com/appwrite/appwrite/pull/10007)
* Update Appwrite description to include Sites, add MCP to products list in [#9867](https://github.com/appwrite/appwrite/pull/9867)
* Update README.md in [#10026](https://github.com/appwrite/appwrite/pull/10026)
* Fix duplication of platforms in swagger specs in [#10008](https://github.com/appwrite/appwrite/pull/10008)
* Update react native sdk and changelog in [#10025](https://github.com/appwrite/appwrite/pull/10025)
* Update delete project signature in [#10028](https://github.com/appwrite/appwrite/pull/10028)
* Fix Golang SDK examples for docs in [#10001](https://github.com/appwrite/appwrite/pull/10001)
* Revert "worker: Graceful shutdown on SIGTERM" in [#10035](https://github.com/appwrite/appwrite/pull/10035)
* Fix benchmark CI in [#10055](https://github.com/appwrite/appwrite/pull/10055)
* Use ->action(...)) instead of ->callback([$this, 'action']); in [#9967](https://github.com/appwrite/appwrite/pull/9967)
* Override project via custom domains log in [#10011](https://github.com/appwrite/appwrite/pull/10011)
* Add database worker job logging in [#10056](https://github.com/appwrite/appwrite/pull/10056)
* Add runtimeEntrypoint param in [#10062](https://github.com/appwrite/appwrite/pull/10062)
* Add missing injections in [#10061](https://github.com/appwrite/appwrite/pull/10061)
* Replace Console loop with Swoole Timer for stats resource m… in [#10054](https://github.com/appwrite/appwrite/pull/10054)
* Update README.md in [#10063](https://github.com/appwrite/appwrite/pull/10063)
* Fix parameter order in action function for robots.txt route in [#10067](https://github.com/appwrite/appwrite/pull/10067)
* Preview endpoint logging in [#10068](https://github.com/appwrite/appwrite/pull/10068)
* Fix flakyness of account tests in [#10066](https://github.com/appwrite/appwrite/pull/10066)
* Update cli to 8.1.0 and add changelog in [#10070](https://github.com/appwrite/appwrite/pull/10070)
* Update composer.json and composer.lock to include appwrite-lab… in [#10051](https://github.com/appwrite/appwrite/pull/10051)
* Fix tests, for `Cloud` in [#10085](https://github.com/appwrite/appwrite/pull/10085)
* Update README.md in [#10084](https://github.com/appwrite/appwrite/pull/10084)
* Revert "chore: update composer.json and composer.lock to include appwrite-lab…" in [#10086](https://github.com/appwrite/appwrite/pull/10086)
* Update README to add Bulk API link in [#10095](https://github.com/appwrite/appwrite/pull/10095)
* Add redis publisher to schedule base if available in [#10099](https://github.com/appwrite/appwrite/pull/10099)
* Fix site template test in [#10104](https://github.com/appwrite/appwrite/pull/10104)
* Update nodejs 17.1.0 in [#10088](https://github.com/appwrite/appwrite/pull/10088)
* Update README.md to add Upsert announcement in [#10112](https://github.com/appwrite/appwrite/pull/10112)
* Reduce delete batch size in [#10128](https://github.com/appwrite/appwrite/pull/10128)
* Update README.md in [#10134](https://github.com/appwrite/appwrite/pull/10134)
* Speed up tests in [#10127](https://github.com/appwrite/appwrite/pull/10127)
* Update cli to 8.2.0 in [#10136](https://github.com/appwrite/appwrite/pull/10136)
* Prevent injected $user from being shadowed in [#10150](https://github.com/appwrite/appwrite/pull/10150)
* Update react native to 0.10.1 and dotnet to 0.14.0 in [#10138](https://github.com/appwrite/appwrite/pull/10138)
* Update README.md in [#10153](https://github.com/appwrite/appwrite/pull/10153)
* Update cli 8.2.1 in [#10155](https://github.com/appwrite/appwrite/pull/10155)
* Fix build usage specification in [#10157](https://github.com/appwrite/appwrite/pull/10157)
* Handle redirect validator in specs + GraphQL type mapper in [#10158](https://github.com/appwrite/appwrite/pull/10158)
* Update dart 16.1.0, flutter 17.0.2 and cli 8.2.2 in [#10161](https://github.com/appwrite/appwrite/pull/10161)
* Improve invalid scheme error in origin check in [#10164](https://github.com/appwrite/appwrite/pull/10164)
* 1.7.x in [#9897](https://github.com/appwrite/appwrite/pull/9897)
* Added the cases of null permissions in the upsert route and update th… in [#10179](https://github.com/appwrite/appwrite/pull/10179)
* Fix 1.7.x specs in [#10197](https://github.com/appwrite/appwrite/pull/10197)
* Suppress git-action exception in deployment worker in [#10199](https://github.com/appwrite/appwrite/pull/10199)
* Stats-usage on redis in [#10156](https://github.com/appwrite/appwrite/pull/10156)
* Fix templates on `1.7.x`. in [#10203](https://github.com/appwrite/appwrite/pull/10203)
* Change preview & body for MFA email in [#10205](https://github.com/appwrite/appwrite/pull/10205)
* Add docs for nestedType, encode, from and toMap in [#10204](https://github.com/appwrite/appwrite/pull/10204)
* Update sdks 1.7.x in [#10202](https://github.com/appwrite/appwrite/pull/10202)
* Update migration release in [#10222](https://github.com/appwrite/appwrite/pull/10222)
* Remove sequence on incoming docs in [#10228](https://github.com/appwrite/appwrite/pull/10228)
* Filter certificates renewal task in maintenance by region in [#10227](https://github.com/appwrite/appwrite/pull/10227)
* Move changelog to sdks platforms array in [#10233](https://github.com/appwrite/appwrite/pull/10233)
* Update changelog and sdk gen in [#10247](https://github.com/appwrite/appwrite/pull/10247)
* Telemetry for cache hits and misses in [#10240](https://github.com/appwrite/appwrite/pull/10240)
* Add model examples + additonal examples to specs in [#10249](https://github.com/appwrite/appwrite/pull/10249)
* Update favicons endpoint to fallback to ico instead of throwing error in [#10260](https://github.com/appwrite/appwrite/pull/10260)
* Update README.md in [#10259](https://github.com/appwrite/appwrite/pull/10259)
* Check CAA record before issuing certificate in [#10258](https://github.com/appwrite/appwrite/pull/10258)
* Revert "Check CAA record before issuing certificate" in [#10263](https://github.com/appwrite/appwrite/pull/10263)
* Test var id attribute in [#10243](https://github.com/appwrite/appwrite/pull/10243)
* Add type attribute to the database creation flow in [#10266](https://github.com/appwrite/appwrite/pull/10266)
* Add CAA validator in [#10267](https://github.com/appwrite/appwrite/pull/10267)
* Update database type to grids and legacy in [#10273](https://github.com/appwrite/appwrite/pull/10273)
* Update README.md in [#10272](https://github.com/appwrite/appwrite/pull/10272)
* Upgrade composer for utopia migration in [#10274](https://github.com/appwrite/appwrite/pull/10274)
* Update SDK generator and sdks in [#10271](https://github.com/appwrite/appwrite/pull/10271)
* Fix wrong resource path for audits in [#10279](https://github.com/appwrite/appwrite/pull/10279)
* Update `grid` on resource events in [#10282](https://github.com/appwrite/appwrite/pull/10282)
* Add readonly param to sequence, databaseId and collectionId in [#10278](https://github.com/appwrite/appwrite/pull/10278)
* Update migrations in [#10283](https://github.com/appwrite/appwrite/pull/10283)
* Add placeholder detection in [#10284](https://github.com/appwrite/appwrite/pull/10284)
* Update docker base to 0.10.3 in [#10285](https://github.com/appwrite/appwrite/pull/10285)
* Make check for adding warning header stricter in [#10293](https://github.com/appwrite/appwrite/pull/10293)
* Fix databases worker cache clearing bug in [#10294](https://github.com/appwrite/appwrite/pull/10294)
* Reapply Redis functions queue in [#10299](https://github.com/appwrite/appwrite/pull/10299)
* Add new database query type tests in [#10296](https://github.com/appwrite/appwrite/pull/10296)
* Update package in [#10312](https://github.com/appwrite/appwrite/pull/10312)
* Update required attributes in [#10311](https://github.com/appwrite/appwrite/pull/10311)
* Remove experiment warnings from bulk methods in [#10310](https://github.com/appwrite/appwrite/pull/10310)
* Update README.md in [#10313](https://github.com/appwrite/appwrite/pull/10313)
* Added internal file param to handle upload to internal bucket in [#10321](https://github.com/appwrite/appwrite/pull/10321)
* Remove temp logging in [#10302](https://github.com/appwrite/appwrite/pull/10302)
* Improve sites test for stability in [#10331](https://github.com/appwrite/appwrite/pull/10331)
* Database lib bump to 0.71.15 in [#10336](https://github.com/appwrite/appwrite/pull/10336)
* Clarify userId param in endpoints that create accounts in [#10117](https://github.com/appwrite/appwrite/pull/10117)
* Upgrade HTTP in [#10338](https://github.com/appwrite/appwrite/pull/10338)
* Remove unnessessary external dependnecies in [#10343](https://github.com/appwrite/appwrite/pull/10343)
* Sync main into 1.7.x in [#10347](https://github.com/appwrite/appwrite/pull/10347)
* Fix TablesDB casing in [#10346](https://github.com/appwrite/appwrite/pull/10346)
* Add cookies test in [#10352](https://github.com/appwrite/appwrite/pull/10352)
* Update token tests with jwt decode in [#10354](https://github.com/appwrite/appwrite/pull/10354)
* Utilize assets server for fonts in [#10358](https://github.com/appwrite/appwrite/pull/10358)
* Sync main into 1.7.x in [#10359](https://github.com/appwrite/appwrite/pull/10359)
* Bump 1.7.x in [#10365](https://github.com/appwrite/appwrite/pull/10365)
* Fix queue health in [#10369](https://github.com/appwrite/appwrite/pull/10369)
* Allow publisher messaging override in scheduler in [#10370](https://github.com/appwrite/appwrite/pull/10370)
* Add replacewith and deprecated since to account methods in [#10377](https://github.com/appwrite/appwrite/pull/10377)
* Update README.md in [#10376](https://github.com/appwrite/appwrite/pull/10376)
* Update CLI in [#10390](https://github.com/appwrite/appwrite/pull/10390)
* Update default method in description in [#10391](https://github.com/appwrite/appwrite/pull/10391)
* Rename namespace from tables-db to tablesdb in specs in [#10395](https://github.com/appwrite/appwrite/pull/10395)
* Update tables group in specs in [#10394](https://github.com/appwrite/appwrite/pull/10394)
* Update description for upsert methods in [#10397](https://github.com/appwrite/appwrite/pull/10397)
* Update README.md in [#10401](https://github.com/appwrite/appwrite/pull/10401)
* Added handling of database resources after migration in [#10400](https://github.com/appwrite/appwrite/pull/10400)
* Revert "Added handling of database resources after migration" in [#10406](https://github.com/appwrite/appwrite/pull/10406)
* Remove sdk deprecation warnings in [#10408](https://github.com/appwrite/appwrite/pull/10408)
* Mark Row response model's param with readonly in [#10409](https://github.com/appwrite/appwrite/pull/10409)
* Update exception thrown when svg sanitization fails in [#10416](https://github.com/appwrite/appwrite/pull/10416)
* Fix allow null params in [#10417](https://github.com/appwrite/appwrite/pull/10417)
* Allow running tests with specific response format in [#10418](https://github.com/appwrite/appwrite/pull/10418)
* Make webhooks publisher overridable in [#10419](https://github.com/appwrite/appwrite/pull/10419)
* Check audits logs in [#10414](https://github.com/appwrite/appwrite/pull/10414)
* Remove direct publisher calls in [#10420](https://github.com/appwrite/appwrite/pull/10420)
* removed spatial type response and will be using the json type for the… in [#10433](https://github.com/appwrite/appwrite/pull/10433)
* Add tests for new time helpers in [#10437](https://github.com/appwrite/appwrite/pull/10437)
* Move projects.list() to module in [#10441](https://github.com/appwrite/appwrite/pull/10441)
* Update cli to 9.1.0 in [#10442](https://github.com/appwrite/appwrite/pull/10442)
* Add requestBody param examples in specs in [#10431](https://github.com/appwrite/appwrite/pull/10431)
* Fix mysql tests in [#10445](https://github.com/appwrite/appwrite/pull/10445)
* Upgrade platform lib to have older queue lib in [#10447](https://github.com/appwrite/appwrite/pull/10447)
* Fix router compression in [#10452](https://github.com/appwrite/appwrite/pull/10452)
* Upgrade http lib for backwards compatible default param in [#10455](https://github.com/appwrite/appwrite/pull/10455)
* Update examples in [#10444](https://github.com/appwrite/appwrite/pull/10444)
* Automatic pr creation in sdk release script in [#10457](https://github.com/appwrite/appwrite/pull/10457)
* Remove avatars command from cli in [#10454](https://github.com/appwrite/appwrite/pull/10454)
* Remove deno from platforms array in [#10453](https://github.com/appwrite/appwrite/pull/10453)
* Spatial type attributes sdk updates in [#10463](https://github.com/appwrite/appwrite/pull/10463)
* Stats resources try catch in [#10469](https://github.com/appwrite/appwrite/pull/10469)
* Move proxy endpoints to modules in [#10470](https://github.com/appwrite/appwrite/pull/10470)
* Add certificate validation override in [#10471](https://github.com/appwrite/appwrite/pull/10471)
* Generate SDKs in [#10475](https://github.com/appwrite/appwrite/pull/10475)
* Spatial test tablesdb updates in [#10473](https://github.com/appwrite/appwrite/pull/10473)
* Add colors to certificate logs in [#10438](https://github.com/appwrite/appwrite/pull/10438)
* appwrite db bump in [#10479](https://github.com/appwrite/appwrite/pull/10479)
* Bump database in [#10480](https://github.com/appwrite/appwrite/pull/10480)
* Health db queues in [#10482](https://github.com/appwrite/appwrite/pull/10482)
* Attempt small size for website dependency in [#10485](https://github.com/appwrite/appwrite/pull/10485)
* Worker stop in [#10498](https://github.com/appwrite/appwrite/pull/10498)
* Update database in [#10506](https://github.com/appwrite/appwrite/pull/10506)
* Stats resources and usage sorting by unique field in [#10472](https://github.com/appwrite/appwrite/pull/10472)
* Add spatial column validation during required mode and tests for exis… in [#10509](https://github.com/appwrite/appwrite/pull/10509)
* Sub query variables order by in [#10513](https://github.com/appwrite/appwrite/pull/10513)
* Update README.md in [#10514](https://github.com/appwrite/appwrite/pull/10514)
* bump database 1.5.0 in [#10515](https://github.com/appwrite/appwrite/pull/10515)
* Don't remove required attributes in [#10516](https://github.com/appwrite/appwrite/pull/10516)
* Catch query exception on bulk update/delete in [#10517](https://github.com/appwrite/appwrite/pull/10517)
* Update cli to 10.0.0 in [#10511](https://github.com/appwrite/appwrite/pull/10511)
* Add type_enum support and update docs in [#10496](https://github.com/appwrite/appwrite/pull/10496)
* Improve code readability for schedules in [#10522](https://github.com/appwrite/appwrite/pull/10522)
* Include response model enum names in [#10538](https://github.com/appwrite/appwrite/pull/10538)
* SDK releases in [#10539](https://github.com/appwrite/appwrite/pull/10539)
* Fix health status enum in [#10540](https://github.com/appwrite/appwrite/pull/10540)
* Update afterbuild fn in [#10541](https://github.com/appwrite/appwrite/pull/10541)
* Update afterbuild to also pass adapter in [#10545](https://github.com/appwrite/appwrite/pull/10545)
* Update `z-index` to be the highest in [#9874](https://github.com/appwrite/appwrite/pull/9874)
* Update framework lib to 0.33.28 in [#10551](https://github.com/appwrite/appwrite/pull/10551)
* Fix enum typing for platform in specs in [#10553](https://github.com/appwrite/appwrite/pull/10553)
* Add enums for database type and column status in [#10561](https://github.com/appwrite/appwrite/pull/10561)
* Fix activities in [#10586](https://github.com/appwrite/appwrite/pull/10586)
* Fix logs truncation tests in [#10585](https://github.com/appwrite/appwrite/pull/10585)
* Remove related data in realtime payload in [#10590](https://github.com/appwrite/appwrite/pull/10590)
* Update composer dependencies in [#10601](https://github.com/appwrite/appwrite/pull/10601)
* Update sdks add response models in [#10554](https://github.com/appwrite/appwrite/pull/10554)
* Sanitize 5xx errors on realtime in [#10598](https://github.com/appwrite/appwrite/pull/10598)
* Update database in [#10596](https://github.com/appwrite/appwrite/pull/10596)
* Add both collection and table id in the realtime in [#10608](https://github.com/appwrite/appwrite/pull/10608)
* Chore bump db in [#10611](https://github.com/appwrite/appwrite/pull/10611)
* Branded email for Console auth flows in [#10501](https://github.com/appwrite/appwrite/pull/10501)
* Add minor releases for all SDKs - deprecate createVerification, add createEmailVerification in [#10614](https://github.com/appwrite/appwrite/pull/10614)
* Add automatic releases in [#10615](https://github.com/appwrite/appwrite/pull/10615)
* Feat txn sdks in [#10621](https://github.com/appwrite/appwrite/pull/10621)
* Prevent empty releases in sdk release script in [#10627](https://github.com/appwrite/appwrite/pull/10627)
* Update domains lib to 0.8.2 in [#10629](https://github.com/appwrite/appwrite/pull/10629)
* Fix txn API scope backwards compat in [#10640](https://github.com/appwrite/appwrite/pull/10640)
* Fix block schedules in [#10620](https://github.com/appwrite/appwrite/pull/10620)
* Update .NET SDK to 0.21.2 and improve release detection in [#10641](https://github.com/appwrite/appwrite/pull/10641)
* Make methods protected for extending in [#10617](https://github.com/appwrite/appwrite/pull/10617)
# Version 1.7.4
## What's Changed
### Notable changes
* Update console image to version 6.0.13 in [#9891](https://github.com/appwrite/appwrite/pull/9891)
### Fixes
* Fix createDeployment chunk upload in [#9886](https://github.com/appwrite/appwrite/pull/9886)
### Miscellaneous
* Update version from 1.7.3 to 1.7.4 in [#9893](https://github.com/appwrite/appwrite/pull/9893)
# Version 1.7.3
## What's Changed
### Notable changes
* Allow unlimited deployment size in [#9866](https://github.com/appwrite/appwrite/pull/9866)
* Bump console to version 6.0.11 in [#9881](https://github.com/appwrite/appwrite/pull/9881)
### Fixes
* Send deploymentResourceType in rules verification in [#9859](https://github.com/appwrite/appwrite/pull/9859)
* Fix CNAME validation in [#9861](https://github.com/appwrite/appwrite/pull/9861)
* Fix bucket not included in path in [#9864](https://github.com/appwrite/appwrite/pull/9864)
* Fix URL for view logs in github comment in [#9875](https://github.com/appwrite/appwrite/pull/9875)
* Set owner and region while migrating rules in [#9856](https://github.com/appwrite/appwrite/pull/9856)
* Remove _APP_DEFAULT_REGION because it is not a valid env var in [#9883](https://github.com/appwrite/appwrite/pull/9883)
### Miscellaneous
* Only load error page for development mode in [#9860](https://github.com/appwrite/appwrite/pull/9860)
* Make max deployment and build size configurable in [#9863](https://github.com/appwrite/appwrite/pull/9863)
* Update flutter_web_auth_2 docs to match 4.x in [#9858](https://github.com/appwrite/appwrite/pull/9858)
* Use unique filename for health check in [#9842](https://github.com/appwrite/appwrite/pull/9842)
* Added encrypt property in the attribute string response model in [#9868](https://github.com/appwrite/appwrite/pull/9868)
* Add sequence in [#9865](https://github.com/appwrite/appwrite/pull/9865)
* Add builds worker group in [#9873](https://github.com/appwrite/appwrite/pull/9873)
* updated errro for the string encryption in [#9878](https://github.com/appwrite/appwrite/pull/9878)
* Revert "Add sequence" in [#9879](https://github.com/appwrite/appwrite/pull/9879)
* Prepare 1.7.3 release in [#9882](https://github.com/appwrite/appwrite/pull/9882)
# Version 1.6.2
## What's Changed
### Notable changes
* Delete git folder to reduce build size in [9076](https://github.com/appwrite/appwrite/pull/9076)
* Upgrade assistant in [9100](https://github.com/appwrite/appwrite/pull/9100)
* Use redis adapter for abuse in [9121](https://github.com/appwrite/appwrite/pull/9121)
* Set base specification CPUs to 0.5 again in [9146](https://github.com/appwrite/appwrite/pull/9146)
* Add new push message parameters in [9060](https://github.com/appwrite/appwrite/pull/9060)
* Update audits to include user type in [9211](https://github.com/appwrite/appwrite/pull/9211)
* Enable HEIC in [9251](https://github.com/appwrite/appwrite/pull/9251)
* Added teamName to membership redirect url in [9269](https://github.com/appwrite/appwrite/pull/9269)
* Add support endpoint url for S3 in [9303](https://github.com/appwrite/appwrite/pull/9303)
* Added RuPay Credit Card Icon in Avatars Service in [5046](https://github.com/appwrite/appwrite/pull/5046)
* Add figma oauth provider in [9623](https://github.com/appwrite/appwrite/pull/9623)
* Update console to version 5.2.58 in [9637](https://github.com/appwrite/appwrite/pull/9637)
* Delete git folder to reduce build size in [#9076](https://github.com/appwrite/appwrite/pull/9076)
* Upgrade assistant in [#9100](https://github.com/appwrite/appwrite/pull/9100)
* Use redis adapter for abuse in [#9121](https://github.com/appwrite/appwrite/pull/9121)
* Set base specification CPUs to 0.5 again in [#9146](https://github.com/appwrite/appwrite/pull/9146)
* Add new push message parameters in [#9060](https://github.com/appwrite/appwrite/pull/9060)
* Update audits to include user type in [#9211](https://github.com/appwrite/appwrite/pull/9211)
* Enable HEIC in [#9251](https://github.com/appwrite/appwrite/pull/9251)
* Added teamName to membership redirect url in [#9269](https://github.com/appwrite/appwrite/pull/9269)
* Add support endpoint url for S3 in [#9303](https://github.com/appwrite/appwrite/pull/9303)
* Added RuPay Credit Card Icon in Avatars Service in [#5046](https://github.com/appwrite/appwrite/pull/5046)
* Add figma oauth provider in [#9623](https://github.com/appwrite/appwrite/pull/9623)
* Update console to version 5.2.58 in [#9637](https://github.com/appwrite/appwrite/pull/9637)
### Fixes
* Remove failed attribute in [9032](https://github.com/appwrite/appwrite/pull/9032)
* Fix delete notFound attribute in [9038](https://github.com/appwrite/appwrite/pull/9038)
* 🇮🇸 Added missing Icelandic translations for email strings. in [4848](https://github.com/appwrite/appwrite/pull/4848)
* fix doc comment for filter method in [5769](https://github.com/appwrite/appwrite/pull/5769)
* Delete attribute No throwing Exception on not found in [9157](https://github.com/appwrite/appwrite/pull/9157)
* Fix VCS identity collision in [9138](https://github.com/appwrite/appwrite/pull/9138)
* Fix disabling of email-otp when user wants to in [9200](https://github.com/appwrite/appwrite/pull/9200)
* Ensure user can delete session in [9209](https://github.com/appwrite/appwrite/pull/9209)
* Fix resend invitation in [9218](https://github.com/appwrite/appwrite/pull/9218)
* Fix phone number parsing exception handling in [9246](https://github.com/appwrite/appwrite/pull/9246)
* Fix amazon oauth in [9253](https://github.com/appwrite/appwrite/pull/9253)
* Fix slack oauth scopes, and updated to v2 in [9228](https://github.com/appwrite/appwrite/pull/9228)
* Fix forwarded user agent in [9271](https://github.com/appwrite/appwrite/pull/9271)
* Fix WEBP File Preview Rendering Issue in [9321](https://github.com/appwrite/appwrite/pull/9321)
* Fix build memory specifications in [9360](https://github.com/appwrite/appwrite/pull/9360)
* Fix Self Hosting functions by adding missed config in [9373](https://github.com/appwrite/appwrite/pull/9373)
* Fix resend team invite if already accepted in [9348](https://github.com/appwrite/appwrite/pull/9348)
* Fix null errors on team invite in [9391](https://github.com/appwrite/appwrite/pull/9391)
* Fix email (smtp) to multiple recipients in [9243](https://github.com/appwrite/appwrite/pull/9243)
* Fix stats timing by using receivedAt date when available in [9428](https://github.com/appwrite/appwrite/pull/9428)
* Make min/max params optional for attribute update in [9387](https://github.com/appwrite/appwrite/pull/9387)
* Fix blocking of phone sessions when disabled on console in [9447](https://github.com/appwrite/appwrite/pull/9447)
* Fix logging config in [9467](https://github.com/appwrite/appwrite/pull/9467)
* Update audit timestamp origin in [9481](https://github.com/appwrite/appwrite/pull/9481)
* Fix certificates in deletes worker in [9466](https://github.com/appwrite/appwrite/pull/9466)
* Fix console audits delete in [9547](https://github.com/appwrite/appwrite/pull/9547)
* Fix migrations in [9633](https://github.com/appwrite/appwrite/pull/9633)
* Ensure all 4xx errors in OAuth redirect lead to the failure URL in [9679](https://github.com/appwrite/appwrite/pull/9679)
* Treat 0 as unlimited for CPUs and memory in [9638](https://github.com/appwrite/appwrite/pull/9638)
* Add contextual dispatch logic to fix high CPU usage in [9687](https://github.com/appwrite/appwrite/pull/9687)
* Remove failed attribute in [#9032](https://github.com/appwrite/appwrite/pull/9032)
* Fix delete notFound attribute in [#9038](https://github.com/appwrite/appwrite/pull/9038)
* 🇮🇸 Added missing Icelandic translations for email strings. in [#4848](https://github.com/appwrite/appwrite/pull/4848)
* fix doc comment for filter method in [#5769](https://github.com/appwrite/appwrite/pull/5769)
* Delete attribute No throwing Exception on not found in [#9157](https://github.com/appwrite/appwrite/pull/9157)
* Fix VCS identity collision in [#9138](https://github.com/appwrite/appwrite/pull/9138)
* Fix disabling of email-otp when user wants to in [#9200](https://github.com/appwrite/appwrite/pull/9200)
* Ensure user can delete session in [#9209](https://github.com/appwrite/appwrite/pull/9209)
* Fix resend invitation in [#9218](https://github.com/appwrite/appwrite/pull/9218)
* Fix phone number parsing exception handling in [#9246](https://github.com/appwrite/appwrite/pull/9246)
* Fix amazon oauth in [#9253](https://github.com/appwrite/appwrite/pull/9253)
* Fix slack oauth scopes, and updated to v2 in [#9228](https://github.com/appwrite/appwrite/pull/9228)
* Fix forwarded user agent in [#9271](https://github.com/appwrite/appwrite/pull/9271)
* Fix WEBP File Preview Rendering Issue in [#9321](https://github.com/appwrite/appwrite/pull/9321)
* Fix build memory specifications in [#9360](https://github.com/appwrite/appwrite/pull/9360)
* Fix Self Hosting functions by adding missed config in [#9373](https://github.com/appwrite/appwrite/pull/9373)
* Fix resend team invite if already accepted in [#9348](https://github.com/appwrite/appwrite/pull/9348)
* Fix null errors on team invite in [#9391](https://github.com/appwrite/appwrite/pull/9391)
* Fix email (smtp) to multiple recipients in [#9243](https://github.com/appwrite/appwrite/pull/9243)
* Fix stats timing by using receivedAt date when available in [#9428](https://github.com/appwrite/appwrite/pull/9428)
* Make min/max params optional for attribute update in [#9387](https://github.com/appwrite/appwrite/pull/9387)
* Fix blocking of phone sessions when disabled on console in [#9447](https://github.com/appwrite/appwrite/pull/9447)
* Fix logging config in [#9467](https://github.com/appwrite/appwrite/pull/9467)
* Update audit timestamp origin in [#9481](https://github.com/appwrite/appwrite/pull/9481)
* Fix certificates in deletes worker in [#9466](https://github.com/appwrite/appwrite/pull/9466)
* Fix console audits delete in [#9547](https://github.com/appwrite/appwrite/pull/9547)
* Fix migrations in [#9633](https://github.com/appwrite/appwrite/pull/9633)
* Ensure all 4xx errors in OAuth redirect lead to the failure URL in [#9679](https://github.com/appwrite/appwrite/pull/9679)
* Treat 0 as unlimited for CPUs and memory in [#9638](https://github.com/appwrite/appwrite/pull/9638)
* Add contextual dispatch logic to fix high CPU usage in [#9687](https://github.com/appwrite/appwrite/pull/9687)
### Miscellaneous
* Merge 1.6.x into feat-custom-cf-hostnames in [8904](https://github.com/appwrite/appwrite/pull/8904)
* Improve compression param checks in [8922](https://github.com/appwrite/appwrite/pull/8922)
* upgrade utopia storage in [8930](https://github.com/appwrite/appwrite/pull/8930)
* Feat migration in [8797](https://github.com/appwrite/appwrite/pull/8797)
* feat fix web routes in [8962](https://github.com/appwrite/appwrite/pull/8962)
* Fix no pool access in [9027](https://github.com/appwrite/appwrite/pull/9027)
* feat: use environment variable to check rules format in [9039](https://github.com/appwrite/appwrite/pull/9039)
* Update storage.php in [9037](https://github.com/appwrite/appwrite/pull/9037)
* Upgrade db 0.53.200 in [9050](https://github.com/appwrite/appwrite/pull/9050)
* Chore: upgrade utopia storage in [9066](https://github.com/appwrite/appwrite/pull/9066)
* Update usage-dump payload in [9085](https://github.com/appwrite/appwrite/pull/9085)
* GitHub Workflows security hardening in [3728](https://github.com/appwrite/appwrite/pull/3728)
* Update add-oauth2-provider.md in [4313](https://github.com/appwrite/appwrite/pull/4313)
* update readme-cn some doc in [5278](https://github.com/appwrite/appwrite/pull/5278)
* Add accessibility features in [7042](https://github.com/appwrite/appwrite/pull/7042)
* Add Appwrite Cloud to read me. in [5445](https://github.com/appwrite/appwrite/pull/5445)
* Migration throw error in [9092](https://github.com/appwrite/appwrite/pull/9092)
* Fix usage payload bug in [9097](https://github.com/appwrite/appwrite/pull/9097)
* chore: replace occurrences of dbForConsole to dbForPlatform in [9096](https://github.com/appwrite/appwrite/pull/9096)
* fix(realtime): decrement connectionCounter only if connection is known in [9055](https://github.com/appwrite/appwrite/pull/9055)
* payload bug fix in [9098](https://github.com/appwrite/appwrite/pull/9098)
* Fix usage payload bug in [9099](https://github.com/appwrite/appwrite/pull/9099)
* Usage payload debug in [9101](https://github.com/appwrite/appwrite/pull/9101)
* Usage payload debug in [9103](https://github.com/appwrite/appwrite/pull/9103)
* Usage payload debug in [9104](https://github.com/appwrite/appwrite/pull/9104)
* Feat: createFunction abuse labels in [9102](https://github.com/appwrite/appwrite/pull/9102)
* Docs-create-document in [9105](https://github.com/appwrite/appwrite/pull/9105)
* Docs: Create document and unknown attribute error messages. in [5427](https://github.com/appwrite/appwrite/pull/5427)
* Fix: update project accessed at from router and schedulers in [9109](https://github.com/appwrite/appwrite/pull/9109)
* chore: initial commit in [9111](https://github.com/appwrite/appwrite/pull/9111)
* chore: optimise webhooks payload in [9115](https://github.com/appwrite/appwrite/pull/9115)
* Revert "chore: initial commit" in [9117](https://github.com/appwrite/appwrite/pull/9117)
* chore: fix attribute name in [9118](https://github.com/appwrite/appwrite/pull/9118)
* Migrate to redis abuse in [9124](https://github.com/appwrite/appwrite/pull/9124)
* Added webhooks usage stats in [9125](https://github.com/appwrite/appwrite/pull/9125)
* chore remove abuse cleanup in [9137](https://github.com/appwrite/appwrite/pull/9137)
* fix: remove abuse delete trigger in [9139](https://github.com/appwrite/appwrite/pull/9139)
* Remove firebase OAuth API endpoints in [9144](https://github.com/appwrite/appwrite/pull/9144)
* chore: release client sdks in [9112](https://github.com/appwrite/appwrite/pull/9112)
* Update general.php in [9155](https://github.com/appwrite/appwrite/pull/9155)
* feat(swoole): allow configuration override of available cpus in [9177](https://github.com/appwrite/appwrite/pull/9177)
* Usage databases api read writes addition in [9142](https://github.com/appwrite/appwrite/pull/9142)
* Fix dead connections in [9190](https://github.com/appwrite/appwrite/pull/9190)
* Add hostname to audits in [9165](https://github.com/appwrite/appwrite/pull/9165)
* chore: shifted authphone usage tracking to api calls in [9191](https://github.com/appwrite/appwrite/pull/9191)
* Revert "Fix dead connections" in [9201](https://github.com/appwrite/appwrite/pull/9201)
* Add assertEventually to messaging provider logs test in [9192](https://github.com/appwrite/appwrite/pull/9192)
* feat project sms usage in [9198](https://github.com/appwrite/appwrite/pull/9198)
* chore: add audit labels to project resources in [9056](https://github.com/appwrite/appwrite/pull/9056)
* fix sms usage in [9207](https://github.com/appwrite/appwrite/pull/9207)
* Update database in [9202](https://github.com/appwrite/appwrite/pull/9202)
* Fix dead connections in [9213](https://github.com/appwrite/appwrite/pull/9213)
* Revert "Fix dead connections" in [9214](https://github.com/appwrite/appwrite/pull/9214)
* Add logs db init for consistency in [9163](https://github.com/appwrite/appwrite/pull/9163)
* Split the collection definitions in [9153](https://github.com/appwrite/appwrite/pull/9153)
* Log path with populated parameters in [9220](https://github.com/appwrite/appwrite/pull/9220)
* Add missing scope on function template in [9208](https://github.com/appwrite/appwrite/pull/9208)
* Add relatedCollection default in [9225](https://github.com/appwrite/appwrite/pull/9225)
* fix: function usage in [9235](https://github.com/appwrite/appwrite/pull/9235)
* feat: optimise events payloads in [9232](https://github.com/appwrite/appwrite/pull/9232)
* Optimise webhook events in [9168](https://github.com/appwrite/appwrite/pull/9168)
* fix: maintenance job missing type in [9238](https://github.com/appwrite/appwrite/pull/9238)
* Update Fetch to 0.3.0 in [9245](https://github.com/appwrite/appwrite/pull/9245)
* Fix maintenance job in [9247](https://github.com/appwrite/appwrite/pull/9247)
* chore: add missing case for executions in [9248](https://github.com/appwrite/appwrite/pull/9248)
* Add index dependency exception in [9226](https://github.com/appwrite/appwrite/pull/9226)
* chore: fix benchmarking test when made from fork in [9233](https://github.com/appwrite/appwrite/pull/9233)
* Update SDK Generator versions in [9188](https://github.com/appwrite/appwrite/pull/9188)
* chore: skipped job instead of throwing error in [9250](https://github.com/appwrite/appwrite/pull/9250)
* Implement new SDK Class on 1.6.x in [9237](https://github.com/appwrite/appwrite/pull/9237)
* Delete collection before Appwrite's attributes in [9256](https://github.com/appwrite/appwrite/pull/9256)
* Feat batch usage dump in [9255](https://github.com/appwrite/appwrite/pull/9255)
* Fix cloud tests in [9261](https://github.com/appwrite/appwrite/pull/9261)
* Usage: Databases reads writes in [9260](https://github.com/appwrite/appwrite/pull/9260)
* Update: Latest sdk specs in [9274](https://github.com/appwrite/appwrite/pull/9274)
* Revert "Feat batch usage dump" in [9276](https://github.com/appwrite/appwrite/pull/9276)
* feat: add fast2SMS adapter in [9263](https://github.com/appwrite/appwrite/pull/9263)
* Update Sdk Generator dependency in [9280](https://github.com/appwrite/appwrite/pull/9280)
* Transformed at addition in [9281](https://github.com/appwrite/appwrite/pull/9281)
* Docs: clarify update endpoints only work on draft messages in [9236](https://github.com/appwrite/appwrite/pull/9236)
* Update sdk generator dependency in [9282](https://github.com/appwrite/appwrite/pull/9282)
* Revert "Transformed at addition" in [9284](https://github.com/appwrite/appwrite/pull/9284)
* replaced init for cloud link in [9285](https://github.com/appwrite/appwrite/pull/9285)
* Add transformed at in [9289](https://github.com/appwrite/appwrite/pull/9289)
* Make migrations use Dynamic keys for destination in [9291](https://github.com/appwrite/appwrite/pull/9291)
* Make sessions limit tests assert eventually in [9298](https://github.com/appwrite/appwrite/pull/9298)
* Chore update database in [9306](https://github.com/appwrite/appwrite/pull/9306)
* feat: add AMQP queues in [9287](https://github.com/appwrite/appwrite/pull/9287)
* fix(test): use assertEventually instead of while(true) in [9308](https://github.com/appwrite/appwrite/pull/9308)
* fix(certificate worker): events are published without queue name in [9309](https://github.com/appwrite/appwrite/pull/9309)
* chore: update utopia-php/queue to 0.8.1 in [9311](https://github.com/appwrite/appwrite/pull/9311)
* chore: update utopia-php/queue to 0.8.2 in [9312](https://github.com/appwrite/appwrite/pull/9312)
* fix(schedule-tasks): revert back to direct pool usage in [9313](https://github.com/appwrite/appwrite/pull/9313)
* feat: custom app schemes in [9262](https://github.com/appwrite/appwrite/pull/9262)
* Revert "feat: custom app schemes" in [9319](https://github.com/appwrite/appwrite/pull/9319)
* Restore "feat: custom app schemes"" in [9320](https://github.com/appwrite/appwrite/pull/9320)
* Revert "Restore "feat: custom app schemes""" in [9323](https://github.com/appwrite/appwrite/pull/9323)
* chore: update dependencies in [9330](https://github.com/appwrite/appwrite/pull/9330)
* Feat: logs DB in [9272](https://github.com/appwrite/appwrite/pull/9272)
* Catch invalid index in [9329](https://github.com/appwrite/appwrite/pull/9329)
* Fix: missing call for image transformations counting in [9342](https://github.com/appwrite/appwrite/pull/9342)
* Fix drop abuse on shared table project delete in [9346](https://github.com/appwrite/appwrite/pull/9346)
* Only run all table mode tests on db update in [9338](https://github.com/appwrite/appwrite/pull/9338)
* Fix: missing periodic metric in [9350](https://github.com/appwrite/appwrite/pull/9350)
* feat(builds): check if function is blocked before building in [9332](https://github.com/appwrite/appwrite/pull/9332)
* feat: batch create audit logs in [9347](https://github.com/appwrite/appwrite/pull/9347)
* Chore: Update migrations in [9355](https://github.com/appwrite/appwrite/pull/9355)
* Fix: metric time was not being written to DB in [9354](https://github.com/appwrite/appwrite/pull/9354)
* Fix patch index validation in [9356](https://github.com/appwrite/appwrite/pull/9356)
* Fix image trnasformation metrics in [9370](https://github.com/appwrite/appwrite/pull/9370)
* Use batch delete in worker in [9375](https://github.com/appwrite/appwrite/pull/9375)
* Fix Model Platform is missing response key: store in [9361](https://github.com/appwrite/appwrite/pull/9361)
* Feat key segmented usage in [9336](https://github.com/appwrite/appwrite/pull/9336)
* Feat messaging metrics in [9353](https://github.com/appwrite/appwrite/pull/9353)
* Fix removed audits for shared v2 in [9388](https://github.com/appwrite/appwrite/pull/9388)
* chore: bump utopia-php/image to 0.8.0 in [9390](https://github.com/appwrite/appwrite/pull/9390)
* Fix outdated CLI commands in documentation in [9122](https://github.com/appwrite/appwrite/pull/9122)
* disable logs display in [9398](https://github.com/appwrite/appwrite/pull/9398)
* Log batches per project in [9403](https://github.com/appwrite/appwrite/pull/9403)
* Batch per project in [9410](https://github.com/appwrite/appwrite/pull/9410)
* Fix: stats resources only queue projects accessed in last 3 hours in [9411](https://github.com/appwrite/appwrite/pull/9411)
* Track options requests in [9397](https://github.com/appwrite/appwrite/pull/9397)
* chore: bump docker-base in [9406](https://github.com/appwrite/appwrite/pull/9406)
* refactor: migrate Realtime::send calls to queueForRealtime in [9325](https://github.com/appwrite/appwrite/pull/9325)
* Revert "Fix: stats resources only queue projects accessed in last 3 hours" in [9424](https://github.com/appwrite/appwrite/pull/9424)
* Remove usage and usage dump in favor of stats-usage and stats-usage-dump in [9339](https://github.com/appwrite/appwrite/pull/9339)
* Fix: disable dual writing in [9429](https://github.com/appwrite/appwrite/pull/9429)
* Disable transformedAt update for console users in [9425](https://github.com/appwrite/appwrite/pull/9425)
* chore: add image transformation stats to usage endpoint in [9393](https://github.com/appwrite/appwrite/pull/9393)
* chore: added timeout to deployment builds in tests in [9426](https://github.com/appwrite/appwrite/pull/9426)
* fix: model for image transformations in usage project in [9442](https://github.com/appwrite/appwrite/pull/9442)
* Feat: calculate database storage in stats-resources in [9443](https://github.com/appwrite/appwrite/pull/9443)
* Activities batch writes in [9438](https://github.com/appwrite/appwrite/pull/9438)
* chore: bump cache 0.12.x in [9412](https://github.com/appwrite/appwrite/pull/9412)
* chore: queue console project for maintenance delete in [9479](https://github.com/appwrite/appwrite/pull/9479)
* chore: added logsdb for deletes worker in [9462](https://github.com/appwrite/appwrite/pull/9462)
* Feat: calculate and log time taken for each project in [9491](https://github.com/appwrite/appwrite/pull/9491)
* chore: update initializing dbForLogs in [9494](https://github.com/appwrite/appwrite/pull/9494)
* Feat bulk audit delete in [9487](https://github.com/appwrite/appwrite/pull/9487)
* Prepare 1.6.2 release in [9499](https://github.com/appwrite/appwrite/pull/9499)
* Regenerate specs in [9497](https://github.com/appwrite/appwrite/pull/9497)
* Regenerate examples in [9498](https://github.com/appwrite/appwrite/pull/9498)
* chore: bump sdk in [9414](https://github.com/appwrite/appwrite/pull/9414)
* update queue to 0.9.* in [9505](https://github.com/appwrite/appwrite/pull/9505)
* Feat improve delete queries in [9507](https://github.com/appwrite/appwrite/pull/9507)
* Feat: Add rule attributes in [9508](https://github.com/appwrite/appwrite/pull/9508)
* Sync main into 1.6.x in [9496](https://github.com/appwrite/appwrite/pull/9496)
* Bump console to version 5.2.53 in [9495](https://github.com/appwrite/appwrite/pull/9495)
* Prepare 1.6.1 release in [9294](https://github.com/appwrite/appwrite/pull/9294)
* Improve delete ordering in [9512](https://github.com/appwrite/appwrite/pull/9512)
* Cleanups in [9511](https://github.com/appwrite/appwrite/pull/9511)
* Feat dynamic regions in [9408](https://github.com/appwrite/appwrite/pull/9408)
* Feat env vars to system lib in [9515](https://github.com/appwrite/appwrite/pull/9515)
* Feat: domains count in [9514](https://github.com/appwrite/appwrite/pull/9514)
* Migration read from db in [9529](https://github.com/appwrite/appwrite/pull/9529)
* feat: add pool telemetry in [9530](https://github.com/appwrite/appwrite/pull/9530)
* Disable PDO persistence since we manage our own pool in [9526](https://github.com/appwrite/appwrite/pull/9526)
* chore: set min operations to 1 for reads and writes in [9536](https://github.com/appwrite/appwrite/pull/9536)
* Remove default region in [9430](https://github.com/appwrite/appwrite/pull/9430)
* Use cursor pagination with bigger limit for maintenance project loop in [9546](https://github.com/appwrite/appwrite/pull/9546)
* chore: stop tests on failure in [9525](https://github.com/appwrite/appwrite/pull/9525)
* chore: only update total count for privileged users in [9554](https://github.com/appwrite/appwrite/pull/9554)
* refactor: initialization of audit retention in [9563](https://github.com/appwrite/appwrite/pull/9563)
* Delete worker queries fixes in [9523](https://github.com/appwrite/appwrite/pull/9523)
* Bump database 0.62.x in [9568](https://github.com/appwrite/appwrite/pull/9568)
* Fix: schedules region filtering in [9577](https://github.com/appwrite/appwrite/pull/9577)
* Deletes worker fix selects for pagination in [9578](https://github.com/appwrite/appwrite/pull/9578)
* Add $permissions for delete documents selects in [9579](https://github.com/appwrite/appwrite/pull/9579)
* chore(audits): return queue pre-fetch results in [9533](https://github.com/appwrite/appwrite/pull/9533)
* Revert "chore(audits): return queue pre-fetch results" in [9586](https://github.com/appwrite/appwrite/pull/9586)
* Feat multi tenant insert in [9573](https://github.com/appwrite/appwrite/pull/9573)
* Add order by for cursor in [9588](https://github.com/appwrite/appwrite/pull/9588)
* Feat update fetch in [9592](https://github.com/appwrite/appwrite/pull/9592)
* Fix tenant casting in [9598](https://github.com/appwrite/appwrite/pull/9598)
* Feat update ws in [9602](https://github.com/appwrite/appwrite/pull/9602)
* Update database in [9603](https://github.com/appwrite/appwrite/pull/9603)
* Fix: image transformation cache in [9608](https://github.com/appwrite/appwrite/pull/9608)
* Remove audit payload in [9610](https://github.com/appwrite/appwrite/pull/9610)
* Sample rate from DSN in [9559](https://github.com/appwrite/appwrite/pull/9559)
* Restrict role change for sole org owner in [9615](https://github.com/appwrite/appwrite/pull/9615)
* chore: update php image to 0.8.1 in [9616](https://github.com/appwrite/appwrite/pull/9616)
* feat: refactor executor setup in [9420](https://github.com/appwrite/appwrite/pull/9420)
* chore: update gitpod.yml config in [9561](https://github.com/appwrite/appwrite/pull/9561)
* chore: update dependencies in [9625](https://github.com/appwrite/appwrite/pull/9625)
* Update migrations lib in [9628](https://github.com/appwrite/appwrite/pull/9628)
* feat: cache telemetry in [9624](https://github.com/appwrite/appwrite/pull/9624)
* Bump console to version 5.2.56 in [9631](https://github.com/appwrite/appwrite/pull/9631)
* Multi region support in [8667](https://github.com/appwrite/appwrite/pull/8667)
* Revert "Multi region support" in [9632](https://github.com/appwrite/appwrite/pull/9632)
* Revert "Revert "Multi region support"" in [9636](https://github.com/appwrite/appwrite/pull/9636)
* Fix tasks in [9644](https://github.com/appwrite/appwrite/pull/9644)
* chore: updated the migration version to 8.6 in [9646](https://github.com/appwrite/appwrite/pull/9646)
* Fix: merge the working of StatsUsage and StatsUsageDump in [9585](https://github.com/appwrite/appwrite/pull/9585)
* Update database in [9643](https://github.com/appwrite/appwrite/pull/9643)
* chore: fix error logging for CLI tasks in [9651](https://github.com/appwrite/appwrite/pull/9651)
* fix: usage test assertion in [9653](https://github.com/appwrite/appwrite/pull/9653)
* Fix keys in [9656](https://github.com/appwrite/appwrite/pull/9656)
* Feat: multi tenant dual writing in [9583](https://github.com/appwrite/appwrite/pull/9583)
* Fix/throwing 400 for null order attributes in [9657](https://github.com/appwrite/appwrite/pull/9657)
* feat: sdk group attribute in [9596](https://github.com/appwrite/appwrite/pull/9596)
* Add configurable function and build size in [9648](https://github.com/appwrite/appwrite/pull/9648)
* feat: update API endpoint in the code examples in [8933](https://github.com/appwrite/appwrite/pull/8933)
* chore: abstract token secret hiding to response model in [9574](https://github.com/appwrite/appwrite/pull/9574)
* chore: update sdks in [9655](https://github.com/appwrite/appwrite/pull/9655)
* feat: allow non-critical events to ignore exceptions when enqueuing the event in [9680](https://github.com/appwrite/appwrite/pull/9680)
* Revert "Add configurable function and build size" in [9681](https://github.com/appwrite/appwrite/pull/9681)
* core: introduce endpoint.docs in specs in [9685](https://github.com/appwrite/appwrite/pull/9685)
* fix: remove content-type header from get request specs in [9666](https://github.com/appwrite/appwrite/pull/9666)
* chore: update flutter sdk in [9691](https://github.com/appwrite/appwrite/pull/9691)
* Merge 1.6.x into feat-custom-cf-hostnames in [#8904](https://github.com/appwrite/appwrite/pull/8904)
* Improve compression param checks in [#8922](https://github.com/appwrite/appwrite/pull/8922)
* upgrade utopia storage in [#8930](https://github.com/appwrite/appwrite/pull/8930)
* Feat migration in [#8797](https://github.com/appwrite/appwrite/pull/8797)
* feat fix web routes in [#8962](https://github.com/appwrite/appwrite/pull/8962)
* Fix no pool access in [#9027](https://github.com/appwrite/appwrite/pull/9027)
* feat: use environment variable to check rules format in [#9039](https://github.com/appwrite/appwrite/pull/9039)
* Update storage.php in [#9037](https://github.com/appwrite/appwrite/pull/9037)
* Upgrade db 0.53.200 in [#9050](https://github.com/appwrite/appwrite/pull/9050)
* Chore: upgrade utopia storage in [#9066](https://github.com/appwrite/appwrite/pull/9066)
* Update usage-dump payload in [#9085](https://github.com/appwrite/appwrite/pull/9085)
* GitHub Workflows security hardening in [#3728](https://github.com/appwrite/appwrite/pull/3728)
* Update add-oauth2-provider.md in [#4313](https://github.com/appwrite/appwrite/pull/4313)
* update readme-cn some doc in [#5278](https://github.com/appwrite/appwrite/pull/5278)
* Add accessibility features in [#7042](https://github.com/appwrite/appwrite/pull/7042)
* Add Appwrite Cloud to read me. in [#5445](https://github.com/appwrite/appwrite/pull/5445)
* Migration throw error in [#9092](https://github.com/appwrite/appwrite/pull/9092)
* Fix usage payload bug in [#9097](https://github.com/appwrite/appwrite/pull/9097)
* chore: replace occurrences of dbForConsole to dbForPlatform in [#9096](https://github.com/appwrite/appwrite/pull/9096)
* fix(realtime): decrement connectionCounter only if connection is known in [#9055](https://github.com/appwrite/appwrite/pull/9055)
* payload bug fix in [#9098](https://github.com/appwrite/appwrite/pull/9098)
* Fix usage payload bug in [#9099](https://github.com/appwrite/appwrite/pull/9099)
* Usage payload debug in [#9101](https://github.com/appwrite/appwrite/pull/9101)
* Usage payload debug in [#9103](https://github.com/appwrite/appwrite/pull/9103)
* Usage payload debug in [#9104](https://github.com/appwrite/appwrite/pull/9104)
* Feat: createFunction abuse labels in [#9102](https://github.com/appwrite/appwrite/pull/9102)
* Docs-create-document in [#9105](https://github.com/appwrite/appwrite/pull/9105)
* Docs: Create document and unknown attribute error messages. in [#5427](https://github.com/appwrite/appwrite/pull/5427)
* Fix: update project accessed at from router and schedulers in [#9109](https://github.com/appwrite/appwrite/pull/9109)
* chore: initial commit in [#9111](https://github.com/appwrite/appwrite/pull/9111)
* chore: optimise webhooks payload in [#9115](https://github.com/appwrite/appwrite/pull/9115)
* Revert "chore: initial commit" in [#9117](https://github.com/appwrite/appwrite/pull/9117)
* chore: fix attribute name in [#9118](https://github.com/appwrite/appwrite/pull/9118)
* Migrate to redis abuse in [#9124](https://github.com/appwrite/appwrite/pull/9124)
* Added webhooks usage stats in [#9125](https://github.com/appwrite/appwrite/pull/9125)
* chore remove abuse cleanup in [#9137](https://github.com/appwrite/appwrite/pull/9137)
* fix: remove abuse delete trigger in [#9139](https://github.com/appwrite/appwrite/pull/9139)
* Remove firebase OAuth API endpoints in [#9144](https://github.com/appwrite/appwrite/pull/9144)
* chore: release client sdks in [#9112](https://github.com/appwrite/appwrite/pull/9112)
* Update general.php in [#9155](https://github.com/appwrite/appwrite/pull/9155)
* feat(swoole): allow configuration override of available cpus in [#9177](https://github.com/appwrite/appwrite/pull/9177)
* Usage databases api read writes addition in [#9142](https://github.com/appwrite/appwrite/pull/9142)
* Fix dead connections in [#9190](https://github.com/appwrite/appwrite/pull/9190)
* Add hostname to audits in [#9165](https://github.com/appwrite/appwrite/pull/9165)
* chore: shifted authphone usage tracking to api calls in [#9191](https://github.com/appwrite/appwrite/pull/9191)
* Revert "Fix dead connections" in [#9201](https://github.com/appwrite/appwrite/pull/9201)
* Add assertEventually to messaging provider logs test in [#9192](https://github.com/appwrite/appwrite/pull/9192)
* feat project sms usage in [#9198](https://github.com/appwrite/appwrite/pull/9198)
* chore: add audit labels to project resources in [#9056](https://github.com/appwrite/appwrite/pull/9056)
* fix sms usage in [#9207](https://github.com/appwrite/appwrite/pull/9207)
* Update database in [#9202](https://github.com/appwrite/appwrite/pull/9202)
* Fix dead connections in [#9213](https://github.com/appwrite/appwrite/pull/9213)
* Revert "Fix dead connections" in [#9214](https://github.com/appwrite/appwrite/pull/9214)
* Add logs db init for consistency in [#9163](https://github.com/appwrite/appwrite/pull/9163)
* Split the collection definitions in [#9153](https://github.com/appwrite/appwrite/pull/9153)
* Log path with populated parameters in [#9220](https://github.com/appwrite/appwrite/pull/9220)
* Add missing scope on function template in [#9208](https://github.com/appwrite/appwrite/pull/9208)
* Add relatedCollection default in [#9225](https://github.com/appwrite/appwrite/pull/9225)
* fix: function usage in [#9235](https://github.com/appwrite/appwrite/pull/9235)
* feat: optimise events payloads in [#9232](https://github.com/appwrite/appwrite/pull/9232)
* Optimise webhook events in [#9168](https://github.com/appwrite/appwrite/pull/9168)
* fix: maintenance job missing type in [#9238](https://github.com/appwrite/appwrite/pull/9238)
* Update Fetch to 0.3.0 in [#9245](https://github.com/appwrite/appwrite/pull/9245)
* Fix maintenance job in [#9247](https://github.com/appwrite/appwrite/pull/9247)
* chore: add missing case for executions in [#9248](https://github.com/appwrite/appwrite/pull/9248)
* Add index dependency exception in [#9226](https://github.com/appwrite/appwrite/pull/9226)
* chore: fix benchmarking test when made from fork in [#9233](https://github.com/appwrite/appwrite/pull/9233)
* Update SDK Generator versions in [#9188](https://github.com/appwrite/appwrite/pull/9188)
* chore: skipped job instead of throwing error in [#9250](https://github.com/appwrite/appwrite/pull/9250)
* Implement new SDK Class on 1.6.x in [#9237](https://github.com/appwrite/appwrite/pull/9237)
* Delete collection before Appwrite's attributes in [#9256](https://github.com/appwrite/appwrite/pull/9256)
* Feat batch usage dump in [#9255](https://github.com/appwrite/appwrite/pull/9255)
* Fix cloud tests in [#9261](https://github.com/appwrite/appwrite/pull/9261)
* Usage: Databases reads writes in [#9260](https://github.com/appwrite/appwrite/pull/9260)
* Update: Latest sdk specs in [#9274](https://github.com/appwrite/appwrite/pull/9274)
* Revert "Feat batch usage dump" in [#9276](https://github.com/appwrite/appwrite/pull/9276)
* feat: add fast2SMS adapter in [#9263](https://github.com/appwrite/appwrite/pull/9263)
* Update Sdk Generator dependency in [#9280](https://github.com/appwrite/appwrite/pull/9280)
* Transformed at addition in [#9281](https://github.com/appwrite/appwrite/pull/9281)
* Docs: clarify update endpoints only work on draft messages in [#9236](https://github.com/appwrite/appwrite/pull/9236)
* Update sdk generator dependency in [#9282](https://github.com/appwrite/appwrite/pull/9282)
* Revert "Transformed at addition" in [#9284](https://github.com/appwrite/appwrite/pull/9284)
* replaced init for cloud link in [#9285](https://github.com/appwrite/appwrite/pull/9285)
* Add transformed at in [#9289](https://github.com/appwrite/appwrite/pull/9289)
* Make migrations use Dynamic keys for destination in [#9291](https://github.com/appwrite/appwrite/pull/9291)
* Make sessions limit tests assert eventually in [#9298](https://github.com/appwrite/appwrite/pull/9298)
* Chore update database in [#9306](https://github.com/appwrite/appwrite/pull/9306)
* feat: add AMQP queues in [#9287](https://github.com/appwrite/appwrite/pull/9287)
* fix(test): use assertEventually instead of while(true) in [#9308](https://github.com/appwrite/appwrite/pull/9308)
* fix(certificate worker): events are published without queue name in [#9309](https://github.com/appwrite/appwrite/pull/9309)
* chore: update utopia-php/queue to 0.8.1 in [#9311](https://github.com/appwrite/appwrite/pull/9311)
* chore: update utopia-php/queue to 0.8.2 in [#9312](https://github.com/appwrite/appwrite/pull/9312)
* fix(schedule-tasks): revert back to direct pool usage in [#9313](https://github.com/appwrite/appwrite/pull/9313)
* feat: custom app schemes in [#9262](https://github.com/appwrite/appwrite/pull/9262)
* Revert "feat: custom app schemes" in [#9319](https://github.com/appwrite/appwrite/pull/9319)
* Restore "feat: custom app schemes"" in [#9320](https://github.com/appwrite/appwrite/pull/9320)
* Revert "Restore "feat: custom app schemes""" in [#9323](https://github.com/appwrite/appwrite/pull/9323)
* chore: update dependencies in [#9330](https://github.com/appwrite/appwrite/pull/9330)
* Feat: logs DB in [#9272](https://github.com/appwrite/appwrite/pull/9272)
* Catch invalid index in [#9329](https://github.com/appwrite/appwrite/pull/9329)
* Fix: missing call for image transformations counting in [#9342](https://github.com/appwrite/appwrite/pull/9342)
* Fix drop abuse on shared table project delete in [#9346](https://github.com/appwrite/appwrite/pull/9346)
* Only run all table mode tests on db update in [#9338](https://github.com/appwrite/appwrite/pull/9338)
* Fix: missing periodic metric in [#9350](https://github.com/appwrite/appwrite/pull/9350)
* feat(builds): check if function is blocked before building in [#9332](https://github.com/appwrite/appwrite/pull/9332)
* feat: batch create audit logs in [#9347](https://github.com/appwrite/appwrite/pull/9347)
* Chore: Update migrations in [#9355](https://github.com/appwrite/appwrite/pull/9355)
* Fix: metric time was not being written to DB in [#9354](https://github.com/appwrite/appwrite/pull/9354)
* Fix patch index validation in [#9356](https://github.com/appwrite/appwrite/pull/9356)
* Fix image trnasformation metrics in [#9370](https://github.com/appwrite/appwrite/pull/9370)
* Use batch delete in worker in [#9375](https://github.com/appwrite/appwrite/pull/9375)
* Fix Model Platform is missing response key: store in [#9361](https://github.com/appwrite/appwrite/pull/9361)
* Feat key segmented usage in [#9336](https://github.com/appwrite/appwrite/pull/9336)
* Feat messaging metrics in [#9353](https://github.com/appwrite/appwrite/pull/9353)
* Fix removed audits for shared v2 in [#9388](https://github.com/appwrite/appwrite/pull/9388)
* chore: bump utopia-php/image to 0.8.0 in [#9390](https://github.com/appwrite/appwrite/pull/9390)
* Fix outdated CLI commands in documentation in [#9122](https://github.com/appwrite/appwrite/pull/9122)
* disable logs display in [#9398](https://github.com/appwrite/appwrite/pull/9398)
* Log batches per project in [#9403](https://github.com/appwrite/appwrite/pull/9403)
* Batch per project in [#9410](https://github.com/appwrite/appwrite/pull/9410)
* Fix: stats resources only queue projects accessed in last 3 hours in [#9411](https://github.com/appwrite/appwrite/pull/9411)
* Track options requests in [#9397](https://github.com/appwrite/appwrite/pull/9397)
* chore: bump docker-base in [#9406](https://github.com/appwrite/appwrite/pull/9406)
* refactor: migrate Realtime::send calls to queueForRealtime in [#9325](https://github.com/appwrite/appwrite/pull/9325)
* Revert "Fix: stats resources only queue projects accessed in last 3 hours" in [#9424](https://github.com/appwrite/appwrite/pull/9424)
* Remove usage and usage dump in favor of stats-usage and stats-usage-dump in [#9339](https://github.com/appwrite/appwrite/pull/9339)
* Fix: disable dual writing in [#9429](https://github.com/appwrite/appwrite/pull/9429)
* Disable transformedAt update for console users in [#9425](https://github.com/appwrite/appwrite/pull/9425)
* chore: add image transformation stats to usage endpoint in [#9393](https://github.com/appwrite/appwrite/pull/9393)
* chore: added timeout to deployment builds in tests in [#9426](https://github.com/appwrite/appwrite/pull/9426)
* fix: model for image transformations in usage project in [#9442](https://github.com/appwrite/appwrite/pull/9442)
* Feat: calculate database storage in stats-resources in [#9443](https://github.com/appwrite/appwrite/pull/9443)
* Activities batch writes in [#9438](https://github.com/appwrite/appwrite/pull/9438)
* chore: bump cache 0.12.x in [#9412](https://github.com/appwrite/appwrite/pull/9412)
* chore: queue console project for maintenance delete in [#9479](https://github.com/appwrite/appwrite/pull/9479)
* chore: added logsdb for deletes worker in [#9462](https://github.com/appwrite/appwrite/pull/9462)
* Feat: calculate and log time taken for each project in [#9491](https://github.com/appwrite/appwrite/pull/9491)
* chore: update initializing dbForLogs in [#9494](https://github.com/appwrite/appwrite/pull/9494)
* Feat bulk audit delete in [#9487](https://github.com/appwrite/appwrite/pull/9487)
* Prepare 1.6.2 release in [#9499](https://github.com/appwrite/appwrite/pull/9499)
* Regenerate specs in [#9497](https://github.com/appwrite/appwrite/pull/9497)
* Regenerate examples in [#9498](https://github.com/appwrite/appwrite/pull/9498)
* chore: bump sdk in [#9414](https://github.com/appwrite/appwrite/pull/9414)
* update queue to 0.9.* in [#9505](https://github.com/appwrite/appwrite/pull/9505)
* Feat improve delete queries in [#9507](https://github.com/appwrite/appwrite/pull/9507)
* Feat: Add rule attributes in [#9508](https://github.com/appwrite/appwrite/pull/9508)
* Sync main into 1.6.x in [#9496](https://github.com/appwrite/appwrite/pull/9496)
* Bump console to version 5.2.53 in [#9495](https://github.com/appwrite/appwrite/pull/9495)
* Prepare 1.6.1 release in [#9294](https://github.com/appwrite/appwrite/pull/9294)
* Improve delete ordering in [#9512](https://github.com/appwrite/appwrite/pull/9512)
* Cleanups in [#9511](https://github.com/appwrite/appwrite/pull/9511)
* Feat dynamic regions in [#9408](https://github.com/appwrite/appwrite/pull/9408)
* Feat env vars to system lib in [#9515](https://github.com/appwrite/appwrite/pull/9515)
* Feat: domains count in [#9514](https://github.com/appwrite/appwrite/pull/9514)
* Migration read from db in [#9529](https://github.com/appwrite/appwrite/pull/9529)
* feat: add pool telemetry in [#9530](https://github.com/appwrite/appwrite/pull/9530)
* Disable PDO persistence since we manage our own pool in [#9526](https://github.com/appwrite/appwrite/pull/9526)
* chore: set min operations to 1 for reads and writes in [#9536](https://github.com/appwrite/appwrite/pull/9536)
* Remove default region in [#9430](https://github.com/appwrite/appwrite/pull/9430)
* Use cursor pagination with bigger limit for maintenance project loop in [#9546](https://github.com/appwrite/appwrite/pull/9546)
* chore: stop tests on failure in [#9525](https://github.com/appwrite/appwrite/pull/9525)
* chore: only update total count for privileged users in [#9554](https://github.com/appwrite/appwrite/pull/9554)
* refactor: initialization of audit retention in [#9563](https://github.com/appwrite/appwrite/pull/9563)
* Delete worker queries fixes in [#9523](https://github.com/appwrite/appwrite/pull/9523)
* Bump database 0.62.x in [#9568](https://github.com/appwrite/appwrite/pull/9568)
* Fix: schedules region filtering in [#9577](https://github.com/appwrite/appwrite/pull/9577)
* Deletes worker fix selects for pagination in [#9578](https://github.com/appwrite/appwrite/pull/9578)
* Add $permissions for delete documents selects in [#9579](https://github.com/appwrite/appwrite/pull/9579)
* chore(audits): return queue pre-fetch results in [#9533](https://github.com/appwrite/appwrite/pull/9533)
* Revert "chore(audits): return queue pre-fetch results" in [#9586](https://github.com/appwrite/appwrite/pull/9586)
* Feat multi tenant insert in [#9573](https://github.com/appwrite/appwrite/pull/9573)
* Add order by for cursor in [#9588](https://github.com/appwrite/appwrite/pull/9588)
* Feat update fetch in [#9592](https://github.com/appwrite/appwrite/pull/9592)
* Fix tenant casting in [#9598](https://github.com/appwrite/appwrite/pull/9598)
* Feat update ws in [#9602](https://github.com/appwrite/appwrite/pull/9602)
* Update database in [#9603](https://github.com/appwrite/appwrite/pull/9603)
* Fix: image transformation cache in [#9608](https://github.com/appwrite/appwrite/pull/9608)
* Remove audit payload in [#9610](https://github.com/appwrite/appwrite/pull/9610)
* Sample rate from DSN in [#9559](https://github.com/appwrite/appwrite/pull/9559)
* Restrict role change for sole org owner in [#9615](https://github.com/appwrite/appwrite/pull/9615)
* chore: update php image to 0.8.1 in [#9616](https://github.com/appwrite/appwrite/pull/9616)
* feat: refactor executor setup in [#9420](https://github.com/appwrite/appwrite/pull/9420)
* chore: update gitpod.yml config in [#9561](https://github.com/appwrite/appwrite/pull/9561)
* chore: update dependencies in [#9625](https://github.com/appwrite/appwrite/pull/9625)
* Update migrations lib in [#9628](https://github.com/appwrite/appwrite/pull/9628)
* feat: cache telemetry in [#9624](https://github.com/appwrite/appwrite/pull/9624)
* Bump console to version 5.2.56 in [#9631](https://github.com/appwrite/appwrite/pull/9631)
* Multi region support in [#8667](https://github.com/appwrite/appwrite/pull/8667)
* Revert "Multi region support" in [#9632](https://github.com/appwrite/appwrite/pull/9632)
* Revert "Revert "Multi region support"" in [#9636](https://github.com/appwrite/appwrite/pull/9636)
* Fix tasks in [#9644](https://github.com/appwrite/appwrite/pull/9644)
* chore: updated the migration version to 8.6 in [#9646](https://github.com/appwrite/appwrite/pull/9646)
* Fix: merge the working of StatsUsage and StatsUsageDump in [#9585](https://github.com/appwrite/appwrite/pull/9585)
* Update database in [#9643](https://github.com/appwrite/appwrite/pull/9643)
* chore: fix error logging for CLI tasks in [#9651](https://github.com/appwrite/appwrite/pull/9651)
* fix: usage test assertion in [#9653](https://github.com/appwrite/appwrite/pull/9653)
* Fix keys in [#9656](https://github.com/appwrite/appwrite/pull/9656)
* Feat: multi tenant dual writing in [#9583](https://github.com/appwrite/appwrite/pull/9583)
* Fix/throwing 400 for null order attributes in [#9657](https://github.com/appwrite/appwrite/pull/9657)
* feat: sdk group attribute in [#9596](https://github.com/appwrite/appwrite/pull/9596)
* Add configurable function and build size in [#9648](https://github.com/appwrite/appwrite/pull/9648)
* feat: update API endpoint in the code examples in [#8933](https://github.com/appwrite/appwrite/pull/8933)
* chore: abstract token secret hiding to response model in [#9574](https://github.com/appwrite/appwrite/pull/9574)
* chore: update sdks in [#9655](https://github.com/appwrite/appwrite/pull/9655)
* feat: allow non-critical events to ignore exceptions when enqueuing the event in [#9680](https://github.com/appwrite/appwrite/pull/9680)
* Revert "Add configurable function and build size" in [#9681](https://github.com/appwrite/appwrite/pull/9681)
* core: introduce endpoint.docs in specs in [#9685](https://github.com/appwrite/appwrite/pull/9685)
* fix: remove content-type header from get request specs in [#9666](https://github.com/appwrite/appwrite/pull/9666)
* chore: update flutter sdk in [#9691](https://github.com/appwrite/appwrite/pull/9691)
# Version 1.6.1
+2 -3
View File
@@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
--no-plugins --no-scripts --prefer-dist \
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
FROM appwrite/base:0.10.4 AS final
FROM appwrite/base:0.10.5 AS final
LABEL maintainer="team@appwrite.io"
@@ -28,8 +28,6 @@ RUN \
apk add boost boost-dev; \
fi
RUN apk add libwebp
WORKDIR /usr/src/code
COPY --from=composer /usr/local/src/vendor /usr/src/code/vendor
@@ -98,6 +96,7 @@ RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/
# Enable Extensions
RUN if [ "$DEBUG" = "true" ]; then cp /usr/src/code/dev/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini; fi
RUN if [ "$DEBUG" = "true" ]; then mkdir -p /tmp/xdebug; fi
RUN if [ "$DEBUG" = "true" ]; then apk add --update --no-cache openssh-client github-cli; fi
RUN if [ "$DEBUG" = "false" ]; then rm -rf /usr/src/code/dev; fi
RUN if [ "$DEBUG" = "false" ]; then rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20230831/xdebug.so; fi
+1 -1
View File
@@ -1,4 +1,4 @@
> We just announced Transactions API for Appwrite Databases - [Learn more](https://appwrite.io/blog/post/announcing-transactions-api)
> We just announced DB operators for Appwrite Databases - [Learn more](https://appwrite.io/blog/post/announcing-db-operators)
> Appwrite Cloud is now Generally Available - [Learn more](https://appwrite.io/cloud-ga)
+18 -10
View File
@@ -40,8 +40,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) {
@@ -59,7 +57,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;
@@ -73,6 +77,7 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) {
$dbForPlatform = new Database($adapter, $cache);
$dbForPlatform
->setAuthorization($authorization)
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', 'console');
@@ -97,7 +102,7 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) {
}
return $dbForPlatform;
}, ['pools', 'cache']);
}, ['pools', 'cache', 'authorization']);
CLI::setResource('console', function () {
return new Document(Config::getParam('console'));
@@ -108,10 +113,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;
}
@@ -144,6 +149,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', ''));
@@ -160,17 +166,18 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
}
$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;
@@ -180,6 +187,7 @@ CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) {
$database = new Database($adapter, $cache);
$database
->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_TASK)
@@ -192,7 +200,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']);
+59 -3
View File
@@ -1,5 +1,6 @@
<?php
use Appwrite\Auth\Auth;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
@@ -172,7 +173,7 @@ return [
'size' => 256,
'signed' => true,
'required' => false,
'default' => 'argon2',
'default' => Auth::DEFAULT_ALGO,
'array' => false,
'filters' => [],
],
@@ -183,7 +184,7 @@ return [
'size' => 65535,
'signed' => true,
'required' => false,
'default' => ['type' => 'argon2', 'memoryCost' => 2048, 'timeCost' => 4, 'threads' => 3],
'default' => Auth::DEFAULT_ALGO_OPTIONS,
'array' => false,
'filters' => ['json'],
],
@@ -363,6 +364,61 @@ return [
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('emailCanonical'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 320,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('emailIsFree'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('emailIsDisposable'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('emailIsCorporate'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('emailIsCanonical'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
@@ -1114,9 +1170,9 @@ return [
[
'$id' => ID::custom('expire'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'required' => false,
'format' => '',
'signed' => false,
'default' => null,
'array' => false,
+1 -1
View File
@@ -2345,7 +2345,7 @@ return [
'$id' => ID::custom('errors'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 65535,
'size' => 1_000_000,
'signed' => true,
'required' => true,
'default' => null,
+2 -1
View File
@@ -4,6 +4,7 @@
* Initializes console project document.
*/
use Appwrite\Auth\Auth;
use Appwrite\Network\Platform;
use Utopia\Database\Helpers\ID;
use Utopia\System\System;
@@ -37,7 +38,7 @@ $console = [
'mockNumbers' => [],
'invites' => System::getEnv('_APP_CONSOLE_INVITES', 'enabled') === 'enabled',
'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user
'duration' => TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled',
'invalidateSessions' => true
],
+27 -1
View File
@@ -202,6 +202,31 @@ return [
]
]
],
'tanstack-start' => [
'key' => 'tanstack-start',
'name' => 'TanStack Start',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'bundleCommand' => 'bash /usr/local/server/helpers/tanstack-start/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/tanstack-start/env.sh',
'adapters' => [
'ssr' => [
'key' => 'ssr',
'buildCommand' => 'npm run build',
'installCommand' => 'npm install',
'outputDirectory' => './dist',
'startCommand' => 'bash helpers/tanstack-start/server.sh',
],
'static' => [
'key' => 'static',
'buildCommand' => 'npm run build',
'installCommand' => 'npm install',
'outputDirectory' => './dist/client',
'startCommand' => 'bash helpers/server.sh',
]
]
],
'remix' => [
'key' => 'remix',
'name' => 'Remix',
@@ -248,7 +273,7 @@ return [
'key' => 'flutter',
'name' => 'Flutter',
'screenshotSleep' => 5000,
'buildRuntime' => 'flutter-3.29',
'buildRuntime' => 'flutter-3.35',
'runtimes' => getVersions($templateRuntimes['FLUTTER']['versions'], 'flutter'),
'adapters' => [
'static' => [
@@ -257,6 +282,7 @@ return [
'installCommand' => 'flutter pub get',
'outputDirectory' => './build/web',
'startCommand' => 'bash helpers/server.sh',
'fallbackFile' => 'index.html'
],
],
],
@@ -2,6 +2,38 @@
<html>
<head>
<link rel="preconnect" href="https://assets.appwrite.io/" crossorigin>
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<style type="text/css">
:root {
color-scheme: light dark;
supported-color-schemes: light dark;
}
@media (prefers-color-scheme: dark ) {
body {
color: #616b7c !important;
background-color: #ffffff !important;
}
a {
color: currentColor !important;
}
a.button {
color: #ffffff !important;
background-color: {{accentColor}} !important;
border-color: {{accentColor}} !important;
}
h1, h2, h3 {
color: #373b4d !important;
}
h4 {
color: #4f5769 !important;
}
p.security-phrase:not(:empty), hr {
border-color: #e8e9f0 !important;
}
}
</style>
<style>
@font-face {
font-family: 'Inter';
@@ -37,7 +69,6 @@
font-family: "Inter", sans-serif;
background-color: #ffffff;
margin: 0;
padding: 0;
}
a {
color: currentColor;
@@ -98,6 +129,7 @@
color: #ffffff;
border-radius: 8px;
height: 48px;
line-height: 24px;
padding: 12px 20px;
box-sizing: border-box;
cursor: pointer;
@@ -153,7 +185,7 @@
<tr>
<td>
<img
height="32px"
height="26px"
src="{{logoUrl}}"
alt="Appwrite logo"
/>
@@ -2,6 +2,38 @@
<html>
<head>
<link rel="preconnect" href="https://assets.appwrite.io/" crossorigin>
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<style type="text/css">
:root {
color-scheme: light dark;
supported-color-schemes: light dark;
}
@media (prefers-color-scheme: dark ) {
body {
color: #616b7c !important;
background-color: #ffffff !important;
}
a {
color: currentColor !important;
}
a.button {
color: #ffffff !important;
background-color: #2D2D31 !important;
border-color: #414146 !important;
}
h1, h2, h3 {
color: #373b4d !important;
}
h4 {
color: #4f5769 !important;
}
p.security-phrase:not(:empty), hr {
border-color: #e8e9f0 !important;
}
}
</style>
<style>
@font-face {
font-family: 'Inter';
@@ -0,0 +1,8 @@
<p>{{hello}}</p>
<p>{{body}}</p>
<p>{{footer}}</p>
<p style="margin-bottom: 32px">
{{thanks}}
<br/>
{{signature}}
</p>
+15
View File
@@ -57,6 +57,21 @@
"emails.recovery.thanks": "Thanks,",
"emails.recovery.buttonText": "Reset password",
"emails.recovery.signature": "{{project}} team",
"emails.csvExport.success.subject": "Your CSV export is ready",
"emails.csvExport.success.preview": "Your data export has been completed successfully.",
"emails.csvExport.success.hello": "Hello {{user}},",
"emails.csvExport.success.body": "Your CSV export is ready to download. Click the button below to download your data export.",
"emails.csvExport.success.footer": "This download link will expire in 1 hour.",
"emails.csvExport.success.thanks": "Thanks,",
"emails.csvExport.success.buttonText": "Download CSV",
"emails.csvExport.success.signature": "Appwrite team",
"emails.csvExport.failure.subject": "Your CSV export failed - file too large",
"emails.csvExport.failure.preview": "Your data export failed because the file size exceeds your plan limit.",
"emails.csvExport.failure.hello": "Hello {{user}},",
"emails.csvExport.failure.body": "Your CSV export could not be completed because the export file size ({{size}}MB) exceeds your plan limit. Please consider upgrading your plan or exporting a smaller dataset.",
"emails.csvExport.failure.footer": "If you have any questions, please contact our support team.",
"emails.csvExport.failure.thanks": "Thanks,",
"emails.csvExport.failure.signature": "{{project}} team",
"emails.invitation.subject": "Invitation to {{team}} Team at {{project}}",
"emails.invitation.preview": "{{owner}} invited you to join {{team}} at {{project}}",
"emails.invitation.hello": "Hello {{user}},",
+16 -16
View File
@@ -11,7 +11,7 @@ return [
[
'key' => 'web',
'name' => 'Web',
'version' => '21.2.1',
'version' => '21.4.0',
'url' => 'https://github.com/appwrite/sdk-for-web',
'package' => 'https://www.npmjs.com/package/appwrite',
'enabled' => true,
@@ -60,7 +60,7 @@ return [
[
'key' => 'flutter',
'name' => 'Flutter',
'version' => '20.2.1',
'version' => '20.3.1',
'url' => 'https://github.com/appwrite/sdk-for-flutter',
'package' => 'https://pub.dev/packages/appwrite',
'enabled' => true,
@@ -79,7 +79,7 @@ return [
[
'key' => 'apple',
'name' => 'Apple',
'version' => '13.2.2',
'version' => '13.4.0',
'url' => 'https://github.com/appwrite/sdk-for-apple',
'package' => 'https://github.com/appwrite/sdk-for-apple',
'enabled' => true,
@@ -116,7 +116,7 @@ return [
[
'key' => 'android',
'name' => 'Android',
'version' => '11.2.1',
'version' => '11.3.0',
'url' => 'https://github.com/appwrite/sdk-for-android',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-android',
'enabled' => true,
@@ -139,7 +139,7 @@ return [
[
'key' => 'react-native',
'name' => 'React Native',
'version' => '0.17.1',
'version' => '0.18.0',
'url' => 'https://github.com/appwrite/sdk-for-react-native',
'package' => 'https://npmjs.com/package/react-native-appwrite',
'enabled' => true,
@@ -207,7 +207,7 @@ return [
[
'key' => 'web',
'name' => 'Console',
'version' => '0.1.1',
'version' => '0.2.0',
'url' => '',
'package' => '',
'enabled' => true,
@@ -226,7 +226,7 @@ return [
[
'key' => 'cli',
'name' => 'Command Line',
'version' => '10.2.1',
'version' => '11.1.1',
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
@@ -262,7 +262,7 @@ return [
[
'key' => 'nodejs',
'name' => 'Node.js',
'version' => '20.2.1',
'version' => '20.3.0',
'url' => 'https://github.com/appwrite/sdk-for-node',
'package' => 'https://www.npmjs.com/package/node-appwrite',
'enabled' => true,
@@ -281,7 +281,7 @@ return [
[
'key' => 'php',
'name' => 'PHP',
'version' => '17.4.1',
'version' => '18.0.1',
'url' => 'https://github.com/appwrite/sdk-for-php',
'package' => 'https://packagist.org/packages/appwrite/appwrite',
'enabled' => true,
@@ -300,7 +300,7 @@ return [
[
'key' => 'python',
'name' => 'Python',
'version' => '13.4.1',
'version' => '13.6.1',
'url' => 'https://github.com/appwrite/sdk-for-python',
'package' => 'https://pypi.org/project/appwrite/',
'enabled' => true,
@@ -319,7 +319,7 @@ return [
[
'key' => 'ruby',
'name' => 'Ruby',
'version' => '19.2.1',
'version' => '19.3.0',
'url' => 'https://github.com/appwrite/sdk-for-ruby',
'package' => 'https://rubygems.org/gems/appwrite',
'enabled' => true,
@@ -338,7 +338,7 @@ return [
[
'key' => 'go',
'name' => 'Go',
'version' => 'v0.13.1',
'version' => 'v0.14.0',
'url' => 'https://github.com/appwrite/sdk-for-go',
'package' => 'https://github.com/appwrite/sdk-for-go',
'enabled' => true,
@@ -357,7 +357,7 @@ return [
[
'key' => 'dotnet',
'name' => '.NET',
'version' => '0.21.2',
'version' => '0.22.0',
'url' => 'https://github.com/appwrite/sdk-for-dotnet',
'package' => 'https://www.nuget.org/packages/Appwrite',
'enabled' => true,
@@ -376,7 +376,7 @@ return [
[
'key' => 'dart',
'name' => 'Dart',
'version' => '19.2.1',
'version' => '19.4.0',
'url' => 'https://github.com/appwrite/sdk-for-dart',
'package' => 'https://pub.dev/packages/dart_appwrite',
'enabled' => true,
@@ -395,7 +395,7 @@ return [
[
'key' => 'kotlin',
'name' => 'Kotlin',
'version' => '12.2.1',
'version' => '12.3.0',
'url' => 'https://github.com/appwrite/sdk-for-kotlin',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-kotlin',
'enabled' => true,
@@ -418,7 +418,7 @@ return [
[
'key' => 'swift',
'name' => 'Swift',
'version' => '13.2.2',
'version' => '13.3.0',
'url' => 'https://github.com/appwrite/sdk-for-swift',
'package' => 'https://github.com/appwrite/sdk-for-swift',
'enabled' => true,
+7 -6
View File
@@ -1,5 +1,6 @@
<?php
use Appwrite\Auth\Auth;
$member = [
'global',
@@ -91,7 +92,7 @@ $admins = [
];
return [
USER_ROLE_GUESTS => [
Auth::USER_ROLE_GUESTS => [
'label' => 'Guests',
'scopes' => [
'global',
@@ -111,23 +112,23 @@ return [
'execution.write',
],
],
USER_ROLE_USERS => [
Auth::USER_ROLE_USERS => [
'label' => 'Users',
'scopes' => \array_merge($member),
],
USER_ROLE_ADMIN => [
Auth::USER_ROLE_ADMIN => [
'label' => 'Admin',
'scopes' => \array_merge($admins),
],
USER_ROLE_DEVELOPER => [
Auth::USER_ROLE_DEVELOPER => [
'label' => 'Developer',
'scopes' => \array_merge($admins),
],
USER_ROLE_OWNER => [
Auth::USER_ROLE_OWNER => [
'label' => 'Owner',
'scopes' => \array_merge($member, $admins),
],
USER_ROLE_APPS => [
Auth::USER_ROLE_APPS => [
'label' => 'Applications',
'scopes' => ['global', 'health.read', 'graphql'],
],
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
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)));
}
+2 -2
View File
@@ -14,7 +14,7 @@ return [
],
'DART' => [
'name' => 'dart',
'versions' => ['3.8', '3.5', '3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16']
'versions' => ['3.9', '3.8', '3.5', '3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16']
],
'GO' => [
'name' => 'go',
@@ -38,6 +38,6 @@ return [
],
'FLUTTER' => [
'name' => 'flutter',
'versions' => ['3.32', '3.24']
'versions' => ['3.35', '3.32', '3.24']
],
];
+108 -1
View File
@@ -9,6 +9,11 @@ use Utopia\System\System;
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_DOMAIN', '');
// Temporary fix until we can set _APP_DOMAIN to "localhost" instead of "traefik"
if (System::getEnv('_APP_ENV', 'development') === 'development') {
$hostname = 'localhost';
}
$url = $protocol . '://' . $hostname;
class UseCases
@@ -19,6 +24,7 @@ class UseCases
public const ECOMMERCE = 'ecommerce';
public const DOCUMENTATION = 'documentation';
public const BLOG = 'blog';
public const AI = 'artificial intelligence';
}
const TEMPLATE_FRAMEWORKS = [
@@ -78,7 +84,7 @@ const TEMPLATE_FRAMEWORKS = [
'installCommand' => '',
'buildCommand' => 'flutter build web',
'outputDirectory' => './build/web',
'buildRuntime' => 'flutter-3.29',
'buildRuntime' => 'flutter-3.35',
'adapter' => 'static',
'fallbackFile' => '',
],
@@ -111,6 +117,16 @@ const TEMPLATE_FRAMEWORKS = [
'outputDirectory' => './dist',
'fallbackFile' => '+not-found.html',
],
'TANSTACK_START' => [
'key' => 'tanstack-start',
'name' => 'TanStack Start',
'installCommand' => 'npm install',
'buildCommand' => 'npm run build',
'outputDirectory' => './dist',
'buildRuntime' => 'node-22',
'adapter' => 'ssr',
'fallbackFile' => '',
],
'ANGULAR' => [
'key' => 'angular',
'name' => 'Angular',
@@ -950,6 +966,50 @@ return [
],
]
],
[
'key' => 'starter-for-tanstack-start',
'name' => 'TanStack Start starter',
'useCases' => [UseCases::STARTER],
'tagline' => 'Simple TanStack Start application integrated with Appwrite SDK.',
'score' => 9, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-tanstack-start-dark.png',
'screenshotLight' => $url . '/images/sites/templates/starter-for-tanstack-start-light.png',
'frameworks' => [
getFramework('TANSTACK_START', [
'providerRootDirectory' => './',
]),
],
'vcsProvider' => 'github',
'providerRepositoryId' => 'starter-for-tanstack-start',
'providerOwner' => 'appwrite',
'providerVersion' => '0.1.*',
'variables' => [
[
'name' => 'VITE_APPWRITE_ENDPOINT',
'description' => 'Endpoint of Appwrite server',
'value' => '{apiEndpoint}',
'placeholder' => '{apiEndpoint}',
'required' => true,
'type' => 'text'
],
[
'name' => 'VITE_APPWRITE_PROJECT_ID',
'description' => 'Your Appwrite project ID',
'value' => '{projectId}',
'placeholder' => '{projectId}',
'required' => true,
'type' => 'text'
],
[
'name' => 'VITE_APPWRITE_PROJECT_NAME',
'description' => 'Your Appwrite project name',
'value' => '{projectName}',
'placeholder' => '{projectName}',
'required' => true,
'type' => 'text'
],
]
],
[
'key' => 'starter-for-nuxt',
'name' => 'Nuxt starter',
@@ -1327,6 +1387,25 @@ return [
'providerVersion' => '0.3.*',
'variables' => [],
],
[
'key' => 'playground-for-tanstack-start',
'name' => 'TanStack Start playground',
'tagline' => 'A basic TanStack Start website without Appwrite SDK integration.',
'score' => 1, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::STARTER],
'screenshotDark' => $url . '/images/sites/templates/playground-for-tanstack-start-dark.png',
'screenshotLight' => $url . '/images/sites/templates/playground-for-tanstack-start-light.png',
'frameworks' => [
getFramework('TANSTACK_START', [
'providerRootDirectory' => './tanstack-start/starter',
]),
],
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.5.*',
'variables' => [],
],
[
'key' => 'playground-for-react-native',
'name' => 'React Native playground',
@@ -1365,4 +1444,32 @@ return [
'providerVersion' => '0.3.*',
'variables' => []
],
[
'key' => 'text-to-speech',
'name' => 'Text-to-speech with ElevenLabs',
'tagline' => 'Next.js app that transforms text into natural, human-like speech using ElevenLabs',
'score' => 10, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::AI],
'screenshotDark' => $url . '/images/sites/templates/text-to-speech-dark.png',
'screenshotLight' => $url . '/images/sites/templates/text-to-speech-light.png',
'frameworks' => [
getFramework('NEXTJS', [
'providerRootDirectory' => './nextjs/text-to-speech',
]),
],
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.6.*',
'variables' => [
[
'name' => 'ELEVENLABS_API_KEY',
'description' => 'Your ElevenLabs API key',
'value' => '',
'placeholder' => 'sk_.....',
'required' => true,
'type' => 'password'
],
]
],
];
+10
View File
@@ -952,6 +952,16 @@ return [
'question' => '',
'filter' => ''
],
[
'name' => '_APP_BROWSER_HOST',
'description' => 'The host used by Appwrite to communicate with the browser service for screenshots.',
'introduction' => '1.8.0',
'default' => 'http://appwrite-browser:3000/v1',
'required' => false,
'overwrite' => true,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_EXECUTOR_RUNTIME_NETWORK',
'description' => 'Deprecated with 0.14.0, use \'OPEN_RUNTIMES_NETWORK\' instead.',
File diff suppressed because it is too large Load Diff
+200 -13
View File
@@ -1,5 +1,6 @@
<?php
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -23,6 +24,8 @@ use Utopia\Fetch\Client;
use Utopia\Image\Image;
use Utopia\Logger\Logger;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\HexColor;
use Utopia\Validator\Range;
@@ -67,9 +70,9 @@ $avatarCallback = function (string $type, string $code, int $width, int $height,
unset($image);
};
$getUserGitHub = function (string $userId, Document $project, Database $dbForProject, Database $dbForPlatform, ?Logger $logger) {
$getUserGitHub = function (string $userId, Document $project, Database $dbForProject, Database $dbForPlatform, Authorization $authorization, ?Logger $logger) {
try {
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
$user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId));
$sessions = $user->getAttribute('sessions', []);
@@ -120,7 +123,7 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry('')));
Authorization::skip(fn () => $dbForProject->updateDocument('sessions', $gitHubSession->getId(), $gitHubSession));
$authorization->skip(fn () => $dbForProject->updateDocument('sessions', $gitHubSession->getId(), $gitHubSession));
$dbForProject->purgeCachedDocument('users', $user->getId());
} catch (Throwable $err) {
@@ -128,7 +131,7 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro
do {
$previousAccessToken = $gitHubSession->getAttribute('providerAccessToken');
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
$user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId));
$sessions = $user->getAttribute('sessions', []);
$gitHubSession = new Document();
@@ -635,6 +638,187 @@ App::get('/v1/avatars/initials')
->file($image->getImageBlob());
});
App::get('/v1/avatars/screenshots')
->desc('Get webpage screenshot')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('usage.metric', METRIC_AVATARS_SCREENSHOTS_GENERATED)
->label('abuse-limit', 60)
->label('cache', true)
->label('cache.resourceType', 'avatar/screenshot')
->label('cache.resource', 'screenshot/{request.url}/{request.width}/{request.height}/{request.scale}/{request.theme}/{request.userAgent}/{request.fullpage}/{request.locale}/{request.timezone}/{request.latitude}/{request.longitude}/{request.accuracy}/{request.touch}/{request.permissions}/{request.sleep}/{request.quality}/{request.output}')
->label('sdk', new Method(
namespace: 'avatars',
group: null,
name: 'getScreenshot',
description: '/docs/references/avatars/get-screenshot.md',
auth: [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('url', '', new URL(['http', 'https']), 'Website URL which you want to capture.')
->param('headers', [], new Assoc(), 'HTTP headers to send with the browser request. Defaults to empty.', true)
->param('viewportWidth', 1280, new Range(1, 1920), 'Browser viewport width. Pass an integer between 1 to 1920. Defaults to 1280.', true)
->param('viewportHeight', 720, new Range(1, 1080), 'Browser viewport height. Pass an integer between 1 to 1080. Defaults to 720.', true)
->param('scale', 1, new Range(0.1, 3, Range::TYPE_FLOAT), 'Browser scale factor. Pass a number between 0.1 to 3. Defaults to 1.', true)
->param('theme', 'light', new WhiteList(['light', 'dark']), 'Browser theme. Pass "light" or "dark". Defaults to "light".', true)
->param('userAgent', '', new Text(512), 'Custom user agent string. Defaults to browser default.', true)
->param('fullpage', false, new Boolean(true), 'Capture full page scroll. Pass 0 for viewport only, or 1 for full page. Defaults to 0.', true)
->param('locale', '', new Text(10), 'Browser locale (e.g., "en-US", "fr-FR"). Defaults to browser default.', true)
->param('timezone', '', new WhiteList(timezone_identifiers_list()), 'IANA timezone identifier (e.g., "America/New_York", "Europe/London"). Defaults to browser default.', true)
->param('latitude', 0, new Range(-90, 90, Range::TYPE_FLOAT), 'Geolocation latitude. Pass a number between -90 to 90. Defaults to 0.', true)
->param('longitude', 0, new Range(-180, 180, Range::TYPE_FLOAT), 'Geolocation longitude. Pass a number between -180 to 180. Defaults to 0.', true)
->param('accuracy', 0, new Range(0, 100000, Range::TYPE_FLOAT), 'Geolocation accuracy in meters. Pass a number between 0 to 100000. Defaults to 0.', true)
->param('touch', false, new Boolean(true), 'Enable touch support. Pass 0 for no touch, or 1 for touch enabled. Defaults to 0.', true)
->param('permissions', [], new ArrayList(new WhiteList(['geolocation', 'camera', 'microphone', 'notifications', 'midi', 'push', 'clipboard-read', 'clipboard-write', 'payment-handler', 'usb', 'bluetooth', 'accelerometer', 'gyroscope', 'magnetometer', 'ambient-light-sensor', 'background-sync', 'persistent-storage', 'screen-wake-lock', 'web-share', 'xr-spatial-tracking'])), 'Browser permissions to grant. Pass an array of permission names like ["geolocation", "camera", "microphone"]. Defaults to empty.', true)
->param('sleep', 0, new Range(0, 10), 'Wait time in seconds before taking the screenshot. Pass an integer between 0 to 10. Defaults to 0.', true)
->param('width', 0, new Range(0, 2000), 'Output image width. Pass 0 to use original width, or an integer between 1 to 2000. Defaults to 0 (original width).', true)
->param('height', 0, new Range(0, 2000), 'Output image height. Pass 0 to use original height, or an integer between 1 to 2000. Defaults to 0 (original height).', true)
->param('quality', -1, new Range(-1, 100), 'Screenshot quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true)
->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true)
->inject('response')
->inject('queueForStatsUsage')
->action(function (string $url, array $headers, int $viewportWidth, int $viewportHeight, float $scale, string $theme, string $userAgent, bool $fullpage, string $locale, string $timezone, float $latitude, float $longitude, float $accuracy, bool $touch, array $permissions, int $sleep, int $width, int $height, int $quality, string $output, Response $response, StatsUsage $queueForStatsUsage) {
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();
$client->setTimeout(30);
$client->addHeader('content-type', Client::CONTENT_TYPE_APPLICATION_JSON);
// Convert indexed array to empty array (should not happen due to Assoc validator)
if (is_array($headers) && count($headers) > 0 && array_keys($headers) === range(0, count($headers) - 1)) {
$headers = [];
}
// Create a new object to ensure proper JSON serialization
$headersObject = new \stdClass();
foreach ($headers as $key => $value) {
$headersObject->$key = $value;
}
// Create the config with headers as an object
// The custom browser service accepts: url, theme, headers, sleep, viewport, userAgent, fullPage, locale, timezoneId, geolocation, hasTouch, scale
$config = [
'url' => $url,
'theme' => $theme,
'headers' => $headersObject,
'sleep' => $sleep * 1000, // Convert seconds to milliseconds
'waitUntil' => 'load',
'viewport' => [
'width' => $viewportWidth,
'height' => $viewportHeight
]
];
// Add scale if not default
if ($scale != 1) {
$config['deviceScaleFactor'] = $scale;
}
// Add optional parameters that were set, preserving arrays as arrays
if (!empty($userAgent)) {
$config['userAgent'] = $userAgent;
}
if ($fullpage) {
$config['fullPage'] = true;
}
if (!empty($locale)) {
$config['locale'] = $locale;
}
if (!empty($timezone)) {
$config['timezoneId'] = $timezone;
}
// Add geolocation if any coordinates are provided
if ($latitude != 0 || $longitude != 0) {
$config['geolocation'] = [
'latitude' => $latitude,
'longitude' => $longitude,
'accuracy' => $accuracy
];
}
if ($touch) {
$config['hasTouch'] = true;
}
// Add permissions if provided (preserve as array)
if (!empty($permissions)) {
$config['permissions'] = $permissions; // Keep as array
}
try {
$browserEndpoint = System::getEnv('_APP_BROWSER_HOST', 'http://appwrite-browser:3000/v1');
$fetchResponse = $client->fetch(
url: $browserEndpoint . '/screenshots',
method: 'POST',
body: $config
);
if ($fetchResponse->getStatusCode() >= 400) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED, 'Screenshot service failed: ' . $fetchResponse->getBody());
}
$screenshot = $fetchResponse->getBody();
if (empty($screenshot)) {
throw new Exception(Exception::AVATAR_IMAGE_NOT_FOUND, 'Screenshot not generated');
}
// Determine if image processing is needed
$needsProcessing = ($width > 0 || $height > 0) || $quality !== -1 || !empty($output);
if ($needsProcessing) {
// Process image with cropping, quality adjustment, or format conversion
$image = new Image($screenshot);
$image->crop($width, $height);
$output = $output ?: 'png'; // Default to PNG if not specified
$resizedScreenshot = $image->output($output, $quality);
unset($image);
} else {
// Return original screenshot without processing
$resizedScreenshot = $screenshot;
$output = 'png'; // Screenshots are typically PNG by default
}
// Set content type based on output format
$outputs = Config::getParam('storage-outputs');
$contentType = $outputs[$output] ?? $outputs['png'];
$queueForStatsUsage->addMetric(METRIC_AVATARS_SCREENSHOTS_GENERATED, 1);
$response
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
->setContentType($contentType)
->file($resizedScreenshot);
} catch (\Throwable $th) {
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED, 'Screenshot generation failed: ' . $th->getMessage());
}
});
App::get('/v1/cards/cloud')
->desc('Get front Of Cloud Card')
->groups(['api', 'avatars'])
@@ -657,8 +841,9 @@ App::get('/v1/cards/cloud')
->inject('contributors')
->inject('employees')
->inject('logger')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
->inject('authorization')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization) use ($getUserGitHub) {
$user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId));
if ($user->isEmpty() && empty($mock)) {
throw new Exception(Exception::USER_NOT_FOUND);
@@ -669,7 +854,7 @@ App::get('/v1/cards/cloud')
$email = $user->getAttribute('email', '');
$createdAt = new \DateTime($user->getCreatedAt());
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger);
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger, $authorization);
$githubName = $gitHub['name'] ?? '';
$githubId = $gitHub['id'] ?? '';
@@ -864,8 +1049,9 @@ App::get('/v1/cards/cloud-back')
->inject('contributors')
->inject('employees')
->inject('logger')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
->inject('authorization')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization) use ($getUserGitHub) {
$user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId));
if ($user->isEmpty() && empty($mock)) {
throw new Exception(Exception::USER_NOT_FOUND);
@@ -875,7 +1061,7 @@ App::get('/v1/cards/cloud-back')
$userId = $user->getId();
$email = $user->getAttribute('email', '');
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger);
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger, $authorization);
$githubId = $gitHub['id'] ?? '';
$isHero = \array_key_exists($email, $heroes);
@@ -942,8 +1128,9 @@ App::get('/v1/cards/cloud-og')
->inject('contributors')
->inject('employees')
->inject('logger')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
->inject('authorization')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization) use ($getUserGitHub) {
$user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId));
if ($user->isEmpty() && empty($mock)) {
throw new Exception(Exception::USER_NOT_FOUND);
@@ -958,7 +1145,7 @@ App::get('/v1/cards/cloud-og')
$email = $user->getAttribute('email', '');
$createdAt = new \DateTime($user->getCreatedAt());
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger);
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger, $authorization);
$githubName = $gitHub['name'] ?? '';
$githubId = $gitHub['id'] ?? '';
+1
View File
@@ -74,6 +74,7 @@ App::get('/v1/console/variables')
// Combine CAA domain with most common flags and tag (no parameters)
'_APP_DOMAIN_TARGET_CAA' => '0 issue "' . System::getEnv('_APP_DOMAIN_TARGET_CAA') . '"',
'_APP_STORAGE_LIMIT' => +System::getEnv('_APP_STORAGE_LIMIT'),
'_APP_COMPUTE_BUILD_TIMEOUT' => +System::getEnv('_APP_COMPUTE_BUILD_TIMEOUT'),
'_APP_COMPUTE_SIZE_LIMIT' => +System::getEnv('_APP_COMPUTE_SIZE_LIMIT'),
'_APP_USAGE_STATS' => System::getEnv('_APP_USAGE_STATS'),
'_APP_VCS_ENABLED' => $isVcsEnabled,
+3 -2
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']
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
&& !(Auth::isPrivilegedUser($authorization->getRoles()) || Auth::isAppUser($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
-5
View File
@@ -37,7 +37,6 @@ App::get('/v1/locale')
$currencies = Config::getParam('locale-currencies');
$output = [];
$ip = $request->getIP();
$time = (60 * 60 * 24 * 45); // 45 days cache
$output['ip'] = $ip;
@@ -68,10 +67,6 @@ App::get('/v1/locale')
$output['currency'] = $currency;
}
$response
->addHeader('Cache-Control', 'public, max-age=' . $time)
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
;
$response->dynamic(new Document($output), Response::MODEL_LOCALE);
});
+307 -112
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;
@@ -49,6 +50,7 @@ use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\JSON;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@@ -80,12 +82,12 @@ App::post('/v1/messaging/providers/mailgun')
->param('name', '', new Text(128), 'Provider name.')
->param('apiKey', '', new Text(0), 'Mailgun API Key.', true)
->param('domain', '', new Text(0), 'Mailgun Domain.', true)
->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true)
->param('isEuRegion', null, new Nullable(new Boolean()), 'Set as EU region.', true)
->param('fromName', '', new Text(128, 0), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128, 0), 'Name set in the reply to field for the mail. Default value is sender name. Reply to name must have reply to email as well.', true)
->param('replyToEmail', '', new Email(), 'Email set in the reply to field for the mail. Default value is sender email. Reply to email must have reply to name as well.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -177,7 +179,7 @@ App::post('/v1/messaging/providers/sendgrid')
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128, 0), 'Name set in the reply to field for the mail. Default value is sender name.', true)
->param('replyToEmail', '', new Email(), 'Email set in the reply to field for the mail. Default value is sender email.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -231,6 +233,88 @@ App::post('/v1/messaging/providers/sendgrid')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::post('/v1/messaging/providers/resend')
->desc('Create Resend provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.create')
->label('audits.resource', 'provider/{response.$id}')
->label('event', 'providers.[providerId].create')
->label('scope', 'providers.write')
->label('resourceType', RESOURCE_TYPE_PROVIDERS)
->label('sdk', new Method(
namespace: 'messaging',
group: 'providers',
name: 'createResendProvider',
description: '/docs/references/messaging/create-resend-provider.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PROVIDER,
)
]
))
->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('name', '', new Text(128), 'Provider name.')
->param('apiKey', '', new Text(0), 'Resend API key.', true)
->param('fromName', '', new Text(128, 0), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128, 0), 'Name set in the reply to field for the mail. Default value is sender name.', true)
->param('replyToEmail', '', new Email(), 'Email set in the reply to field for the mail. Default value is sender email.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
->action(function (string $providerId, string $name, string $apiKey, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
$providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
$credentials = [];
if (!empty($apiKey)) {
$credentials['apiKey'] = $apiKey;
}
$options = [
'fromName' => $fromName,
'fromEmail' => $fromEmail,
'replyToName' => $replyToName,
'replyToEmail' => $replyToEmail,
];
if (
$enabled === true
&& !empty($fromEmail)
&& \array_key_exists('apiKey', $credentials)
) {
$enabled = true;
} else {
$enabled = false;
}
$provider = new Document([
'$id' => $providerId,
'name' => $name,
'provider' => 'resend',
'type' => MESSAGE_TYPE_EMAIL,
'enabled' => $enabled,
'credentials' => $credentials,
'options' => $options,
]);
try {
$provider = $dbForProject->createDocument('providers', $provider);
} catch (DuplicateException) {
throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
}
$queueForEvents
->setParam('providerId', $provider->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::post('/v1/messaging/providers/smtp')
->desc('Create SMTP provider')
->groups(['api', 'messaging'])
@@ -284,7 +368,7 @@ App::post('/v1/messaging/providers/smtp')
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128, 0), 'Name set in the reply to field for the mail. Default value is sender name.', true)
->param('replyToEmail', '', new Email(), 'Email set in the reply to field for the mail. Default value is sender email.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -371,7 +455,7 @@ App::post('/v1/messaging/providers/msg91')
->param('templateId', '', new Text(0), 'Msg91 template ID', true)
->param('senderId', '', new Text(0), 'Msg91 sender ID.', true)
->param('authKey', '', new Text(0), 'Msg91 auth key.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -454,7 +538,7 @@ App::post('/v1/messaging/providers/telesign')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('customerId', '', new Text(0), 'Telesign customer ID.', true)
->param('apiKey', '', new Text(0), 'Telesign API key.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -538,7 +622,7 @@ App::post('/v1/messaging/providers/textmagic')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('username', '', new Text(0), 'Textmagic username.', true)
->param('apiKey', '', new Text(0), 'Textmagic apiKey.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -622,7 +706,7 @@ App::post('/v1/messaging/providers/twilio')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('accountSid', '', new Text(0), 'Twilio account secret ID.', true)
->param('authToken', '', new Text(0), 'Twilio authentication token.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -706,7 +790,7 @@ App::post('/v1/messaging/providers/vonage')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('apiKey', '', new Text(0), 'Vonage API key.', true)
->param('apiSecret', '', new Text(0), 'Vonage API secret.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -806,8 +890,8 @@ App::post('/v1/messaging/providers/fcm')
])
->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('name', '', new Text(128), 'Provider name.')
->param('serviceAccountJSON', null, new JSON(), 'FCM service account JSON.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('serviceAccountJSON', null, new Nullable(new JSON()), 'FCM service account JSON.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -900,7 +984,7 @@ App::post('/v1/messaging/providers/apns')
->param('teamId', '', new Text(0), 'APNS team ID.', true)
->param('bundleId', '', new Text(0), 'APNS bundle ID.', true)
->param('sandbox', false, new Boolean(), 'Use APNS sandbox environment.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -985,9 +1069,11 @@ App::get('/v1/messaging/providers')
))
->param('queries', [], new Providers(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Providers::ALLOWED_ATTRIBUTES), true)
->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, 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) {
@@ -1013,7 +1099,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.");
@@ -1023,7 +1109,7 @@ App::get('/v1/messaging/providers')
}
try {
$providers = $dbForProject->find('providers', $queries);
$total = $dbForProject->count('providers', $queries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('providers', $queries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
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.");
}
@@ -1053,11 +1139,12 @@ App::get('/v1/messaging/providers/:providerId/logs')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $providerId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->action(function (string $providerId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
$provider = $dbForProject->getDocument('providers', $providerId);
if ($provider->isEmpty()) {
@@ -1125,7 +1212,7 @@ App::get('/v1/messaging/providers/:providerId/logs')
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $includeTotal ? $audit->countLogsByResource($resource, $queries) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -1186,8 +1273,8 @@ App::patch('/v1/messaging/providers/mailgun/:providerId')
->param('name', '', new Text(128), 'Provider name.', true)
->param('apiKey', '', new Text(0), 'Mailgun API Key.', true)
->param('domain', '', new Text(0), 'Mailgun Domain.', true)
->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('isEuRegion', null, new Nullable(new Boolean()), 'Set as EU region.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('fromName', '', new Text(128), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128), 'Name set in the reply to field for the mail. Default value is sender name.', true)
@@ -1297,7 +1384,7 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('apiKey', '', new Text(0), 'Sendgrid API key.', true)
->param('fromName', '', new Text(128), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
@@ -1372,6 +1459,104 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId')
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::patch('/v1/messaging/providers/resend/:providerId')
->desc('Update Resend provider')
->groups(['api', 'messaging'])
->label('audits.event', 'provider.update')
->label('audits.resource', 'provider/{response.$id}')
->label('event', 'providers.[providerId].update')
->label('scope', 'providers.write')
->label('resourceType', RESOURCE_TYPE_PROVIDERS)
->label('sdk', new Method(
namespace: 'messaging',
group: 'providers',
name: 'updateResendProvider',
description: '/docs/references/messaging/update-resend-provider.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROVIDER,
)
]
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('apiKey', '', new Text(0), 'Resend API key.', true)
->param('fromName', '', new Text(128), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name.', true)
->param('replyToEmail', '', new Text(128), 'Email set in the Reply To field for the mail. Default value is Sender Email.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
->action(function (string $providerId, string $name, ?bool $enabled, string $apiKey, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, Event $queueForEvents, Database $dbForProject, Response $response) {
$provider = $dbForProject->getDocument('providers', $providerId);
if ($provider->isEmpty()) {
throw new Exception(Exception::PROVIDER_NOT_FOUND);
}
$providerAttr = $provider->getAttribute('provider');
if ($providerAttr !== 'resend') {
throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
}
if (!empty($name)) {
$provider->setAttribute('name', $name);
}
$options = $provider->getAttribute('options');
if (!empty($fromName)) {
$options['fromName'] = $fromName;
}
if (!empty($fromEmail)) {
$options['fromEmail'] = $fromEmail;
}
if (!empty($replyToName)) {
$options['replyToName'] = $replyToName;
}
if (!empty($replyToEmail)) {
$options['replyToEmail'] = $replyToEmail;
}
$provider->setAttribute('options', $options);
if (!empty($apiKey)) {
$provider->setAttribute('credentials', [
'apiKey' => $apiKey,
]);
}
if (!\is_null($enabled)) {
if ($enabled) {
if (
\array_key_exists('apiKey', $provider->getAttribute('credentials')) &&
\array_key_exists('fromEmail', $provider->getAttribute('options'))
) {
$provider->setAttribute('enabled', true);
} else {
throw new Exception(Exception::PROVIDER_MISSING_CREDENTIALS);
}
} else {
$provider->setAttribute('enabled', false);
}
}
$provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
$queueForEvents
->setParam('providerId', $provider->getId());
$response
->dynamic($provider, Response::MODEL_PROVIDER);
});
App::patch('/v1/messaging/providers/smtp/:providerId')
->desc('Update SMTP provider')
->groups(['api', 'messaging'])
@@ -1415,17 +1600,17 @@ App::patch('/v1/messaging/providers/smtp/:providerId')
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('host', '', new Text(0), 'SMTP hosts. Either a single hostname or multiple semicolon-delimited hostnames. You can also specify a different port for each host such as `smtp1.example.com:25;smtp2.example.com`. You can also specify encryption type, for example: `tls://smtp1.example.com:587;ssl://smtp2.example.com:465"`. Hosts will be tried in order.', true)
->param('port', null, new Range(1, 65535), 'SMTP port.', true)
->param('port', null, new Nullable(new Range(1, 65535)), 'SMTP port.', true)
->param('username', '', new Text(0), 'Authentication username.', true)
->param('password', '', new Text(0), 'Authentication password.', true)
->param('encryption', '', new WhiteList(['none', 'ssl', 'tls']), 'Encryption type. Can be \'ssl\' or \'tls\'', true)
->param('autoTLS', null, new Boolean(), 'Enable SMTP AutoTLS feature.', true)
->param('autoTLS', null, new Nullable(new Boolean()), 'Enable SMTP AutoTLS feature.', true)
->param('mailer', '', new Text(0), 'The value to use for the X-Mailer header.', true)
->param('fromName', '', new Text(128), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name.', true)
->param('replyToEmail', '', new Text(128), 'Email set in the Reply To field for the mail. Default value is Sender Email.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -1543,7 +1728,7 @@ App::patch('/v1/messaging/providers/msg91/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('templateId', '', new Text(0), 'Msg91 template ID.', true)
->param('senderId', '', new Text(0), 'Msg91 sender ID.', true)
->param('authKey', '', new Text(0), 'Msg91 auth key.', true)
@@ -1630,7 +1815,7 @@ App::patch('/v1/messaging/providers/telesign/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('customerId', '', new Text(0), 'Telesign customer ID.', true)
->param('apiKey', '', new Text(0), 'Telesign API key.', true)
->param('from', '', new Text(256), 'Sender number.', true)
@@ -1719,7 +1904,7 @@ App::patch('/v1/messaging/providers/textmagic/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('username', '', new Text(0), 'Textmagic username.', true)
->param('apiKey', '', new Text(0), 'Textmagic apiKey.', true)
->param('from', '', new Text(256), 'Sender number.', true)
@@ -1808,7 +1993,7 @@ App::patch('/v1/messaging/providers/twilio/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('accountSid', '', new Text(0), 'Twilio account secret ID.', true)
->param('authToken', '', new Text(0), 'Twilio authentication token.', true)
->param('from', '', new Text(256), 'Sender number.', true)
@@ -1897,7 +2082,7 @@ App::patch('/v1/messaging/providers/vonage/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('apiKey', '', new Text(0), 'Vonage API key.', true)
->param('apiSecret', '', new Text(0), 'Vonage API secret.', true)
->param('from', '', new Text(256), 'Sender number.', true)
@@ -2005,8 +2190,8 @@ App::patch('/v1/messaging/providers/fcm/:providerId')
])
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('serviceAccountJSON', null, new JSON(), 'FCM service account JSON.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('serviceAccountJSON', null, new Nullable(new JSON()), 'FCM service account JSON.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -2100,12 +2285,12 @@ App::patch('/v1/messaging/providers/apns/:providerId')
])
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('authKey', '', new Text(0), 'APNS authentication key.', true)
->param('authKeyId', '', new Text(0), 'APNS authentication key ID.', true)
->param('teamId', '', new Text(0), 'APNS team ID.', true)
->param('bundleId', '', new Text(0), 'APNS bundle ID.', true)
->param('sandbox', null, new Boolean(), 'Use APNS sandbox environment.', true)
->param('sandbox', null, new Nullable(new Boolean()), 'Use APNS sandbox environment.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -2292,9 +2477,11 @@ App::get('/v1/messaging/topics')
))
->param('queries', [], new Topics(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Topics::ALLOWED_ATTRIBUTES), true)
->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, 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) {
@@ -2320,7 +2507,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.");
@@ -2330,7 +2517,7 @@ App::get('/v1/messaging/topics')
}
try {
$topics = $dbForProject->find('topics', $queries);
$total = $dbForProject->count('topics', $queries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('topics', $queries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
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.");
}
@@ -2360,11 +2547,12 @@ App::get('/v1/messaging/topics/:topicId/logs')
))
->param('topicId', '', new UID(), 'Topic ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $topicId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->action(function (string $topicId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
$topic = $dbForProject->getDocument('topics', $topicId);
if ($topic->isEmpty()) {
@@ -2433,7 +2621,7 @@ App::get('/v1/messaging/topics/:topicId/logs')
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $includeTotal ? $audit->countLogsByResource($resource, $queries) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -2492,8 +2680,8 @@ App::patch('/v1/messaging/topics/:topicId')
]
))
->param('topicId', '', new UID(), 'Topic ID.')
->param('name', null, new Text(128), 'Topic Name.', true)
->param('subscribe', null, new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with subscribe permission. By default all users are granted with any subscribe permission. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true)
->param('name', null, new Nullable(new Text(128)), 'Topic Name.', true)
->param('subscribe', null, new Nullable(new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE)), 'An array of role strings with subscribe permission. By default all users are granted with any subscribe permission. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -2595,29 +2783,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,
@@ -2650,7 +2836,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,
@@ -2693,9 +2879,11 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
->param('topicId', '', new UID(), 'Topic ID. The topic ID subscribed to.')
->param('queries', [], new Subscribers(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Providers::ALLOWED_ATTRIBUTES), true)
->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, 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) {
@@ -2706,7 +2894,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);
@@ -2729,7 +2917,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.");
@@ -2743,10 +2931,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)
@@ -2757,7 +2945,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
$response
->dynamic(new Document([
'subscribers' => $subscribers,
'total' => $dbForProject->count('subscribers', $queries, APP_LIMIT_COUNT),
'total' => $includeTotal ? $dbForProject->count('subscribers', $queries, APP_LIMIT_COUNT) : 0,
]), Response::MODEL_SUBSCRIBER_LIST);
});
@@ -2781,11 +2969,12 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs')
))
->param('subscriberId', '', new UID(), 'Subscriber ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $subscriberId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->action(function (string $subscriberId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
$subscriber = $dbForProject->getDocument('subscribers', $subscriberId);
if ($subscriber->isEmpty()) {
@@ -2854,7 +3043,7 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs')
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $includeTotal ? $audit->countLogsByResource($resource, $queries) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -2880,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);
@@ -2894,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)
@@ -2931,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);
@@ -2956,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,
@@ -3004,7 +3195,7 @@ App::post('/v1/messaging/messages/email')
->param('attachments', [], new ArrayList(new CompoundUID()), 'Array of compound ID strings of bucket IDs and file IDs to be attached to the email. They should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('draft', false, new Boolean(), 'Is message a draft', true)
->param('html', false, new Boolean(), 'Is content of type HTML', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
@@ -3177,7 +3368,7 @@ App::post('/v1/messaging/messages/sms')
->param('users', [], new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', [], new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('draft', false, new Boolean(), 'Is message a draft', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
@@ -3300,7 +3491,7 @@ App::post('/v1/messaging/messages/push')
->param('topics', [], new ArrayList(new UID()), 'List of Topic IDs.', true)
->param('users', [], new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', [], new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('data', null, new JSON(), 'Additional key-value pair data for push notification.', true)
->param('data', null, new Nullable(new JSON()), 'Additional key-value pair data for push notification.', true)
->param('action', '', new Text(256), 'Action for push notification.', true)
->param('image', '', new CompoundUID(), 'Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('icon', '', new Text(256), 'Icon for push notification. Available only for Android and Web Platform.', true)
@@ -3309,7 +3500,7 @@ App::post('/v1/messaging/messages/push')
->param('tag', '', new Text(256), 'Tag for push notification. Available only for Android Platform.', true)
->param('badge', -1, new Integer(), 'Badge for push notification. Available only for iOS Platform.', true)
->param('draft', false, new Boolean(), 'Is message a draft', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('contentAvailable', false, new Boolean(), 'If set to true, the notification will be delivered in the background. Available only for iOS Platform.', true)
->param('critical', false, new Boolean(), 'If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.', true)
->param('priority', 'high', new WhiteList(['normal', 'high']), 'Set the notification priority. "normal" will consider device state and may not deliver notifications immediately. "high" will always attempt to immediately deliver the notification.', true)
@@ -3511,9 +3702,11 @@ App::get('/v1/messaging/messages')
))
->param('queries', [], new Messages(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Messages::ALLOWED_ATTRIBUTES), true)
->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, 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) {
@@ -3539,7 +3732,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.");
@@ -3549,7 +3742,7 @@ App::get('/v1/messaging/messages')
}
try {
$messages = $dbForProject->find('messages', $queries);
$total = $dbForProject->count('messages', $queries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('messages', $queries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
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.");
}
@@ -3579,11 +3772,12 @@ App::get('/v1/messaging/messages/:messageId/logs')
))
->param('messageId', '', new UID(), 'Message ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $messageId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->action(function (string $messageId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -3652,7 +3846,7 @@ App::get('/v1/messaging/messages/:messageId/logs')
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $includeTotal ? $audit->countLogsByResource($resource, $queries) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -3677,9 +3871,10 @@ App::get('/v1/messaging/messages/:messageId/targets')
))
->param('messageId', '', new UID(), 'Message ID.')
->param('queries', [], new Targets(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Targets::ALLOWED_ATTRIBUTES), true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForProject')
->action(function (string $messageId, array $queries, Response $response, Database $dbForProject) {
->action(function (string $messageId, array $queries, bool $includeTotal, Response $response, Database $dbForProject) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -3729,7 +3924,7 @@ App::get('/v1/messaging/messages/:messageId/targets')
}
try {
$targets = $dbForProject->find('targets', $queries);
$total = $dbForProject->count('targets', $queries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('targets', $queries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
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.");
}
@@ -3792,17 +3987,17 @@ App::patch('/v1/messaging/messages/email/:messageId')
]
))
->param('messageId', '', new UID(), 'Message ID.')
->param('topics', null, new ArrayList(new UID()), 'List of Topic IDs.', true)
->param('users', null, new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', null, new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('subject', null, new Text(998), 'Email Subject.', true)
->param('content', null, new Text(64230), 'Email Content.', true)
->param('draft', null, new Boolean(), 'Is message a draft', true)
->param('html', null, new Boolean(), 'Is content of type HTML', true)
->param('cc', null, new ArrayList(new UID()), 'Array of target IDs to be added as CC.', true)
->param('bcc', null, new ArrayList(new UID()), 'Array of target IDs to be added as BCC.', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('attachments', null, new ArrayList(new CompoundUID()), 'Array of compound ID strings of bucket IDs and file IDs to be attached to the email. They should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('topics', null, new Nullable(new ArrayList(new UID())), 'List of Topic IDs.', true)
->param('users', null, new Nullable(new ArrayList(new UID())), 'List of User IDs.', true)
->param('targets', null, new Nullable(new ArrayList(new UID())), 'List of Targets IDs.', true)
->param('subject', null, new Nullable(new Text(998)), 'Email Subject.', true)
->param('content', null, new Nullable(new Text(64230)), 'Email Content.', true)
->param('draft', null, new Nullable(new Boolean()), 'Is message a draft', true)
->param('html', null, new Nullable(new Boolean()), 'Is content of type HTML', true)
->param('cc', null, new Nullable(new ArrayList(new UID())), 'Array of target IDs to be added as CC.', true)
->param('bcc', null, new Nullable(new ArrayList(new UID())), 'Array of target IDs to be added as BCC.', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('attachments', null, new Nullable(new ArrayList(new CompoundUID())), 'Array of compound ID strings of bucket IDs and file IDs to be attached to the email. They should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
@@ -4018,12 +4213,12 @@ App::patch('/v1/messaging/messages/sms/:messageId')
)
])
->param('messageId', '', new UID(), 'Message ID.')
->param('topics', null, new ArrayList(new UID()), 'List of Topic IDs.', true)
->param('users', null, new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', null, new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('content', null, new Text(64230), 'Email Content.', true)
->param('draft', null, new Boolean(), 'Is message a draft', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('topics', null, new Nullable(new ArrayList(new UID())), 'List of Topic IDs.', true)
->param('users', null, new Nullable(new ArrayList(new UID())), 'List of User IDs.', true)
->param('targets', null, new Nullable(new ArrayList(new UID())), 'List of Targets IDs.', true)
->param('content', null, new Nullable(new Text(64230)), 'Email Content.', true)
->param('draft', null, new Nullable(new Boolean()), 'Is message a draft', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
@@ -4180,24 +4375,24 @@ App::patch('/v1/messaging/messages/push/:messageId')
]
))
->param('messageId', '', new UID(), 'Message ID.')
->param('topics', null, new ArrayList(new UID()), 'List of Topic IDs.', true)
->param('users', null, new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', null, new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('title', null, new Text(256), 'Title for push notification.', true)
->param('body', null, new Text(64230), 'Body for push notification.', true)
->param('data', null, new JSON(), 'Additional Data for push notification.', true)
->param('action', null, new Text(256), 'Action for push notification.', true)
->param('image', null, new CompoundUID(), 'Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('icon', null, new Text(256), 'Icon for push notification. Available only for Android and Web platforms.', true)
->param('sound', null, new Text(256), 'Sound for push notification. Available only for Android and iOS platforms.', true)
->param('color', null, new Text(256), 'Color for push notification. Available only for Android platforms.', true)
->param('tag', null, new Text(256), 'Tag for push notification. Available only for Android platforms.', true)
->param('badge', null, new Integer(), 'Badge for push notification. Available only for iOS platforms.', true)
->param('draft', null, new Boolean(), 'Is message a draft', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('contentAvailable', null, new Boolean(), 'If set to true, the notification will be delivered in the background. Available only for iOS Platform.', true)
->param('critical', null, new Boolean(), 'If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.', true)
->param('priority', null, new WhiteList(['normal', 'high']), 'Set the notification priority. "normal" will consider device battery state and may send notifications later. "high" will always attempt to immediately deliver the notification.', true)
->param('topics', null, new Nullable(new ArrayList(new UID())), 'List of Topic IDs.', true)
->param('users', null, new Nullable(new ArrayList(new UID())), 'List of User IDs.', true)
->param('targets', null, new Nullable(new ArrayList(new UID())), 'List of Targets IDs.', true)
->param('title', null, new Nullable(new Text(256)), 'Title for push notification.', true)
->param('body', null, new Nullable(new Text(64230)), 'Body for push notification.', true)
->param('data', null, new Nullable(new JSON()), 'Additional Data for push notification.', true)
->param('action', null, new Nullable(new Text(256)), 'Action for push notification.', true)
->param('image', null, new Nullable(new CompoundUID()), 'Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('icon', null, new Nullable(new Text(256)), 'Icon for push notification. Available only for Android and Web platforms.', true)
->param('sound', null, new Nullable(new Text(256)), 'Sound for push notification. Available only for Android and iOS platforms.', true)
->param('color', null, new Nullable(new Text(256)), 'Color for push notification. Available only for Android platforms.', true)
->param('tag', null, new Nullable(new Text(256)), 'Tag for push notification. Available only for Android platforms.', true)
->param('badge', null, new Nullable(new Integer()), 'Badge for push notification. Available only for iOS platforms.', true)
->param('draft', null, new Nullable(new Boolean()), 'Is message a draft', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('contentAvailable', null, new Nullable(new Boolean()), 'If set to true, the notification will be delivered in the background. Available only for iOS Platform.', true)
->param('critical', null, new Nullable(new Boolean()), 'If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.', true)
->param('priority', null, new Nullable(new WhiteList(['normal', 'high'])), 'Set the notification priority. "normal" will consider device battery state and may send notifications later. "high" will always attempt to immediately deliver the notification.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
+156 -22
View File
@@ -20,6 +20,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\Queries\Documents;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Migration\Resource;
@@ -307,7 +308,8 @@ App::post('/v1/migrations/nhost')
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::post('/v1/migrations/csv')
App::post('/v1/migrations/csv/imports')
->alias('/v1/migrations/csv')
->groups(['api', 'migrations'])
->desc('Import documents from a CSV')
->label('scope', 'migrations.write')
@@ -316,8 +318,8 @@ App::post('/v1/migrations/csv')
->label('sdk', new Method(
namespace: 'migrations',
group: null,
name: 'createCsvMigration',
description: '/docs/references/migrations/migration-csv.md',
name: 'createCSVImport',
description: '/docs/references/migrations/migration-csv-import.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
@@ -333,29 +335,30 @@ App::post('/v1/migrations/csv')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->inject('deviceForFiles')
->inject('deviceForImports')
->inject('deviceForMigrations')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (string $bucketId, string $fileId, string $resourceId, bool $internalFile, Response $response, Database $dbForProject, Database $dbForPlatform, Document $project, Device $deviceForFiles, Device $deviceForImports, Event $queueForEvents, Migration $queueForMigrations) {
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
->action(function (string $bucketId, string $fileId, string $resourceId, bool $internalFile, Response $response, Database $dbForProject, Database $dbForPlatform, Authorization $authorization, Document $project, Device $deviceForFiles, Device $deviceForMigrations, Event $queueForEvents, Migration $queueForMigrations) {
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($internalFile && !$isPrivilegedUser) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$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');
}
return $dbForProject->getDocument('buckets', $bucketId);
});
if ($bucket->isEmpty() || (!$isAPIKey && !$isPrivilegedUser)) {
if ($bucket->isEmpty()) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$file = Authorization::skip(fn () => $internalFile ? $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $fileId) : $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
$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);
}
@@ -365,18 +368,17 @@ App::post('/v1/migrations/csv')
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND, 'File not found in ' . $path);
}
// no encryption, compression on files above 20MB.
// No encryption or compression on files above 20MB.
$hasEncryption = !empty($file->getAttribute('openSSLCipher'));
$compression = $file->getAttribute('algorithm', Compression::NONE);
$hasCompression = $compression !== Compression::NONE;
$migrationId = ID::unique();
$newPath = $deviceForImports->getPath($migrationId . '_' . $fileId . '.csv');
$newPath = $deviceForMigrations->getPath($migrationId . '_' . $fileId . '.csv');
if ($hasEncryption || $hasCompression) {
$source = $deviceForFiles->read($path);
// 1. decrypt
if ($hasEncryption) {
$source = OpenSSL::decrypt(
$source,
@@ -388,7 +390,6 @@ App::post('/v1/migrations/csv')
);
}
// 2. decompress
if ($hasCompression) {
switch ($compression) {
case Compression::ZSTD:
@@ -400,15 +401,15 @@ App::post('/v1/migrations/csv')
}
}
// manual write after decryption and/or decompression
if (! $deviceForImports->write($newPath, $source, 'text/csv')) {
throw new \Exception("Unable to copy file");
// Manual write after decryption and/or decompression
if (!$deviceForMigrations->write($newPath, $source, 'text/csv')) {
throw new \Exception('Unable to copy file');
}
} elseif (! $deviceForFiles->transfer($path, $newPath, $deviceForImports)) {
throw new \Exception("Unable to copy file");
} elseif (!$deviceForFiles->transfer($path, $newPath, $deviceForMigrations)) {
throw new \Exception('Unable to copy file');
}
$fileSize = $deviceForImports->getFileSize($newPath);
$fileSize = $deviceForMigrations->getFileSize($newPath);
$resources = Transfer::extractServices([Transfer::GROUP_DATABASES]);
$migration = $dbForProject->createDocument('migrations', new Document([
@@ -441,6 +442,138 @@ App::post('/v1/migrations/csv')
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::post('/v1/migrations/csv/exports')
->groups(['api', 'migrations'])
->desc('Export documents to CSV')
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].create')
->label('audits.event', 'migration.create')
->label('sdk', new Method(
namespace: 'migrations',
group: null,
name: 'createCSVExport',
description: '/docs/references/migrations/migration-csv-export.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_MIGRATION,
)
]
))
->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database to export.')
->param('filename', '', new Text(255), 'The name of the file to be created for the export, excluding the .csv extension.')
->param('columns', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of attributes to export. If empty, all attributes will be exported. You can use the `*` wildcard to export all attributes from the collection.', true)
->param('queries', [], new ArrayList(new Text(0)), 'Array of query strings generated using the Query class provided by the SDK to filter documents to export. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
->param('delimiter', ',', new Text(1), 'The character that separates each column value. Default is comma.', true)
->param('enclosure', '"', new Text(1), 'The character that encloses each column value. Default is double quotes.', true)
->param('escape', '"', new Text(1), 'The escape character for the enclosure character. Default is double quotes.', true)
->param('header', true, new Boolean(), 'Whether to include the header row with column names. Default is true.', true)
->param('notify', true, new Boolean(), 'Set to true to receive an email when the export is complete. Default is true.', true)
->inject('user')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (
string $resourceId,
string $filename,
array $columns,
array $queries,
string $delimiter,
string $enclosure,
string $escape,
bool $header,
bool $notify,
Document $user,
Response $response,
Database $dbForProject,
Database $dbForPlatform,
Authorization $authorization,
Document $project,
Event $queueForEvents,
Migration $queueForMigrations
) {
try {
$parsedQueries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$bucket = $authorization->skip(fn () => $dbForPlatform->getDocument('buckets', 'default'));
if ($bucket->isEmpty()) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
[$databaseId, $collectionId] = \explode(':', $resourceId, 2);
if (empty($databaseId)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
if (empty($collectionId)) {
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty()) {
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
$validator = new Documents(
attributes: $collection->getAttribute('attributes', []),
indexes: $collection->getAttribute('indexes', []),
idAttributeType: $dbForProject->getAdapter()->getIdAttributeType(),
);
if (!$validator->isValid($parsedQueries)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
'stage' => 'init',
'source' => Appwrite::getName(),
'destination' => CSV::getName(),
'resources' => Transfer::extractServices([Transfer::GROUP_DATABASES]),
'resourceId' => $resourceId,
'resourceType' => Resource::TYPE_DATABASE,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
'options' => [
'bucketId' => 'default', // Always use internal bucket
'filename' => $filename,
'columns' => $columns,
'queries' => $queries,
'delimiter' => $delimiter,
'enclosure' => $enclosure,
'escape' => $escape,
'header' => $header,
'notify' => $notify,
'userInternalId' => $user->getSequence(),
],
]));
$queueForEvents->setParam('migrationId', $migration->getId());
$queueForMigrations
->setMigration($migration)
->setProject($project)
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::get('/v1/migrations')
->groups(['api', 'migrations'])
->desc('List migrations')
@@ -460,9 +593,10 @@ App::get('/v1/migrations')
))
->param('queries', [], new Migrations(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Migrations::ALLOWED_ATTRIBUTES), true)
->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('response')
->inject('dbForProject')
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
->action(function (array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@@ -501,7 +635,7 @@ App::get('/v1/migrations')
$filterQueries = Query::groupByType($queries)['filters'];
try {
$migrations = $dbForProject->find('migrations', $queries);
$total = $dbForProject->count('migrations', $filterQueries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('migrations', $filterQueries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
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.");
}
+8 -6
View File
@@ -18,6 +18,7 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Datetime as DateTimeValidator;
use Utopia\Database\Validator\UID;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@@ -44,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);
@@ -101,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;
@@ -285,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),
@@ -293,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),
@@ -526,8 +528,8 @@ App::put('/v1/project/variables/:variableId')
))
->param('variableId', '', new UID(), 'Variable unique ID.', false)
->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false)
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', true)
->param('secret', null, new Boolean(), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true)
->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
->inject('project')
->inject('response')
->inject('dbForProject')
+19 -14
View File
@@ -1,6 +1,7 @@
<?php
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\MockNumber;
use Appwrite\Event\Delete;
use Appwrite\Event\Mail;
@@ -45,6 +46,7 @@ use Utopia\Validator\Boolean;
use Utopia\Validator\Hostname;
use Utopia\Validator\Integer;
use Utopia\Validator\Multiple;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
@@ -117,7 +119,7 @@ App::post('/v1/projects')
'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT,
'passwordHistory' => 0,
'passwordDictionary' => false,
'duration' => TOKEN_EXPIRATION_LOGIN_LONG,
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG,
'personalDataCheck' => false,
'mockNumbers' => [],
'sessionAlerts' => false,
@@ -677,9 +679,9 @@ App::patch('/v1/projects/:projectId/oauth2')
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'Provider Name')
->param('appId', null, new Text(256), 'Provider app ID. Max length: 256 chars.', true)
->param('secret', null, new text(512), 'Provider secret key. Max length: 512 chars.', true)
->param('enabled', null, new Boolean(), 'Provider status. Set to \'false\' to disable new session creation.', true)
->param('appId', null, new Nullable(new Text(256)), 'Provider app ID. Max length: 256 chars.', true)
->param('secret', null, new Nullable(new text(512)), 'Provider secret key. Max length: 512 chars.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Provider status. Set to \'false\' to disable new session creation.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $provider, ?string $appId, ?string $secret, ?bool $enabled, Response $response, Database $dbForPlatform) {
@@ -1233,9 +1235,10 @@ App::get('/v1/projects/:projectId/webhooks')
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, Response $response, Database $dbForPlatform) {
->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
@@ -1250,7 +1253,7 @@ App::get('/v1/projects/:projectId/webhooks')
$response->dynamic(new Document([
'webhooks' => $webhooks,
'total' => count($webhooks),
'total' => $includeTotal ? count($webhooks) : 0,
]), Response::MODEL_WEBHOOK_LIST);
});
@@ -1474,8 +1477,8 @@ 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 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('expire', null, 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)
->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('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
@@ -1530,9 +1533,10 @@ App::get('/v1/projects/:projectId/keys')
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, Response $response, Database $dbForPlatform) {
->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
@@ -1547,7 +1551,7 @@ App::get('/v1/projects/:projectId/keys')
$response->dynamic(new Document([
'keys' => $keys,
'total' => count($keys),
'total' => $includeTotal ? count($keys) : 0,
]), Response::MODEL_KEY_LIST);
});
@@ -1612,8 +1616,8 @@ 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 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('expire', null, 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)
->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('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
@@ -1833,9 +1837,10 @@ App::get('/v1/projects/:projectId/platforms')
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, Response $response, Database $dbForPlatform) {
->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
@@ -1850,7 +1855,7 @@ App::get('/v1/projects/:projectId/platforms')
$response->dynamic(new Document([
'platforms' => $platforms,
'total' => count($platforms),
'total' => $includeTotal ? count($platforms) : 0,
]), Response::MODEL_PLATFORM_LIST);
});
+115 -110
View File
@@ -32,6 +32,7 @@ use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Authorization\Input;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
@@ -50,6 +51,7 @@ use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\HexColor;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@@ -77,7 +79,7 @@ App::post('/v1/storage/buckets')
))
->param('bucketId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('name', '', new Text(128), 'Bucket name')
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE)), 'An array of permission strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('fileSecurity', false, new Boolean(true), 'Enables configuring permissions for individual file. A user needs one of file or bucket level permissions to access a file. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true)
->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan'])
@@ -180,9 +182,10 @@ App::get('/v1/storage/buckets')
))
->param('queries', [], new Buckets(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Buckets::ALLOWED_ATTRIBUTES), true)
->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('response')
->inject('dbForProject')
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
->action(function (array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject) {
try {
$queries = Query::parseQueries($queries);
@@ -222,7 +225,7 @@ App::get('/v1/storage/buckets')
$filterQueries = Query::groupByType($queries)['filters'];
try {
$buckets = $dbForProject->find('buckets', $queries);
$total = $dbForProject->count('buckets', $filterQueries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('buckets', $filterQueries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
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.");
} catch (QueryException $e) {
@@ -289,7 +292,7 @@ App::put('/v1/storage/buckets/:bucketId')
))
->param('bucketId', '', new UID(), 'Bucket unique ID.')
->param('name', null, new Text(128), 'Bucket name', false)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE)), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('fileSecurity', false, new Boolean(true), 'Enables configuring permissions for individual file. A user needs one of file or bucket level permissions to access a file. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true)
->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan'])
@@ -417,7 +420,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).')
->param('fileId', '', new CustomId(), 'File ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('file', [], new File(), 'Binary file. Appwrite SDKs provide helpers to handle file input. [Learn about file input](https://appwrite.io/docs/products/storage/upload-download#input-file).', skipValidation: true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permission strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE])), 'An array of permission strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->inject('request')
->inject('response')
->inject('dbForProject')
@@ -426,20 +429,20 @@ App::post('/v1/storage/buckets/:bucketId/files')
->inject('mode')
->inject('deviceForFiles')
->inject('deviceForLocal')
->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, string $mode, Device $deviceForFiles, Device $deviceForLocal) {
->inject('authorization')
->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, string $mode, Device $deviceForFiles, Device $deviceForLocal, Authorization $authorization) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$validator = new Authorization(Database::PERMISSION_CREATE);
if (!$validator->isValid($bucket->getCreate())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
if (!$authorization->isValid(new Input(Database::PERMISSION_CREATE, $bucket->getCreate()))) {
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}
$allowedPermissions = [
@@ -462,7 +465,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
}
// Users can only manage their own roles, API keys and Admin users can manage any
$roles = Authorization::getRoles();
$roles = $authorization->getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) {
@@ -475,7 +478,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
$permission->getIdentifier(),
$permission->getDimension()
))->toString();
if (!Authorization::isRole($role)) {
if (!$authorization->hasRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')');
}
}
@@ -695,11 +698,10 @@ App::post('/v1/storage/buckets/:bucketId/files')
* However as with chunk upload even if we are updating, we are essentially creating a file
* adding it's new chunk so we validate create permission instead of update
*/
$validator = new Authorization(Database::PERMISSION_CREATE);
if (!$validator->isValid($bucket->getCreate())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
if (!$authorization->isValid(new Input(Database::PERMISSION_CREATE, $bucket->getCreate()))) {
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}
$file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, $file));
$file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, $file));
}
} else {
if ($file->isEmpty()) {
@@ -738,13 +740,12 @@ App::post('/v1/storage/buckets/:bucketId/files')
* However as with chunk upload even if we are updating, we are essentially creating a file
* adding it's new chunk so we validate create permission instead of update
*/
$validator = new Authorization(Database::PERMISSION_CREATE);
if (!$validator->isValid($bucket->getCreate())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
if (!$authorization->isValid(new Input(Database::PERMISSION_CREATE, $bucket->getCreate()))) {
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}
try {
$file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, $file));
$file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, $file));
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
@@ -785,24 +786,25 @@ App::get('/v1/storage/buckets/:bucketId/files')
->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).')
->param('queries', [], new Files(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Files::ALLOWED_ATTRIBUTES), true)
->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('response')
->inject('dbForProject')
->inject('authorization')
->inject('mode')
->action(function (string $bucketId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
->action(function (string $bucketId, array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject, Authorization $authorization, string $mode) {
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$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) {
throw new Exception(Exception::USER_UNAUTHORIZED);
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}
$queries = Query::parseQueries($queries);
@@ -831,7 +833,7 @@ App::get('/v1/storage/buckets/:bucketId/files')
if ($fileSecurity && !$valid) {
$cursorDocument = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId);
} else {
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
$cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
}
if ($cursorDocument->isEmpty()) {
@@ -841,15 +843,13 @@ App::get('/v1/storage/buckets/:bucketId/files')
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
if ($fileSecurity && !$valid) {
$files = $dbForProject->find('bucket_' . $bucket->getSequence(), $queries);
$total = $dbForProject->count('bucket_' . $bucket->getSequence(), $filterQueries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('bucket_' . $bucket->getSequence(), $queries, APP_LIMIT_COUNT) : 0;
} else {
$files = Authorization::skip(fn () => $dbForProject->find('bucket_' . $bucket->getSequence(), $queries));
$total = Authorization::skip(fn () => $dbForProject->count('bucket_' . $bucket->getSequence(), $filterQueries, APP_LIMIT_COUNT));
$files = $authorization->skip(fn () => $dbForProject->find('bucket_' . $bucket->getSequence(), $queries));
$total = $includeTotal ? $authorization->skip(fn () => $dbForProject->count('bucket_' . $bucket->getSequence(), $queries, APP_LIMIT_COUNT)) : 0;
}
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -888,28 +888,28 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId')
->param('fileId', '', new UID(), 'File ID.')
->inject('response')
->inject('dbForProject')
->inject('authorization')
->inject('mode')
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, string $mode) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Authorization $authorization, string $mode) {
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$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) {
throw new Exception(Exception::USER_UNAUTHORIZED);
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}
if ($fileSecurity && !$valid) {
$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 ($file->isEmpty()) {
@@ -965,17 +965,18 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
->inject('deviceForFiles')
->inject('deviceForLocal')
->inject('project')
->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, ?string $token, Request $request, Response $response, Database $dbForProject, Document $resourceToken, Device $deviceForFiles, Device $deviceForLocal, Document $project) {
->inject('authorization')
->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, ?string $token, Request $request, Response $response, Database $dbForProject, Document $resourceToken, Device $deviceForFiles, Device $deviceForLocal, Document $project, Authorization $authorization) {
if (!\extension_loaded('imagick')) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
}
/* @type Document $bucket */
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -983,21 +984,20 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
$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);
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}
if ($fileSecurity && !$valid && !$isToken) {
$file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId);
} else {
/* @type Document $file */
$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()) {
throw new Exception(Exception::USER_UNAUTHORIZED);
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}
if ($file->isEmpty()) {
@@ -1115,11 +1115,11 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'];
//Do not update transformedAt if it's a console user
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
if (!Auth::isPrivilegedUser($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));
}
}
@@ -1159,15 +1159,16 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('authorization')
->inject('mode')
->inject('resourceToken')
->inject('deviceForFiles')
->action(function (string $bucketId, string $fileId, ?string $token, Request $request, Response $response, Database $dbForProject, string $mode, Document $resourceToken, Device $deviceForFiles) {
->action(function (string $bucketId, string $fileId, ?string $token, Request $request, Response $response, Database $dbForProject, Authorization $authorization, string $mode, Document $resourceToken, Device $deviceForFiles) {
/* @type Document $bucket */
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -1175,21 +1176,20 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
$isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
$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);
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}
if ($fileSecurity && !$valid && !$isToken) {
$file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId);
} else {
/* @type Document $file */
$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()) {
throw new Exception(Exception::USER_UNAUTHORIZED);
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}
if ($file->isEmpty()) {
@@ -1323,12 +1323,13 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
->inject('mode')
->inject('resourceToken')
->inject('deviceForFiles')
->action(function (string $bucketId, string $fileId, ?string $token, Response $response, Request $request, Database $dbForProject, string $mode, Document $resourceToken, Device $deviceForFiles) {
->inject('authorization')
->action(function (string $bucketId, string $fileId, ?string $token, Response $response, Request $request, Database $dbForProject, string $mode, Document $resourceToken, Device $deviceForFiles, Authorization $authorization) {
/* @type Document $bucket */
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -1336,21 +1337,20 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
$isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
$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);
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}
if ($fileSecurity && !$valid && !$isToken) {
$file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId);
} else {
/* @type Document $file */
$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()) {
throw new Exception(Exception::USER_UNAUTHORIZED);
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}
if ($file->isEmpty()) {
@@ -1475,18 +1475,18 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
->inject('response')
->inject('request')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('mode')
->inject('deviceForFiles')
->action(function (string $bucketId, string $fileId, string $jwt, Response $response, Request $request, Database $dbForProject, Document $project, string $mode, Device $deviceForFiles) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
->inject('authorization')
->action(function (string $bucketId, string $fileId, string $jwt, Response $response, Request $request, Database $dbForProject, Database $dbForPlatform, Document $project, string $mode, Device $deviceForFiles, Authorization $authorization) {
$decoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
try {
$decoded = $decoder->decode($jwt);
} catch (JWTException) {
throw new Exception(Exception::USER_UNAUTHORIZED);
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}
if (
@@ -1494,18 +1494,21 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
$decoded['bucketId'] !== $bucketId ||
$decoded['fileId'] !== $fileId
) {
throw new Exception(Exception::USER_UNAUTHORIZED);
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isInternal = $decoded['internal'] ?? false;
$dbForProject = $isInternal ? $dbForPlatform : $dbForProject;
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
$file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
@@ -1513,7 +1516,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
$mimes = Config::getParam('storage-mimes');
$path = $file->getAttribute('path', '');
if (!$deviceForFiles->exists($path)) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND, 'File not found in ' . $path);
}
@@ -1643,33 +1645,33 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
))
->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).')
->param('fileId', '', new UID(), 'File unique ID.')
->param('name', null, new Text(255), 'Name of the file', true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permission string. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('name', null, new Nullable(new Text(255)), 'Name of the file', true)
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE])), 'An array of permission string. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->inject('response')
->inject('dbForProject')
->inject('user')
->inject('mode')
->inject('queueForEvents')
->action(function (string $bucketId, string $fileId, ?string $name, ?array $permissions, Response $response, Database $dbForProject, Document $user, string $mode, Event $queueForEvents) {
->inject('authorization')
->action(function (string $bucketId, string $fileId, ?string $name, ?array $permissions, Response $response, Database $dbForProject, Document $user, string $mode, Event $queueForEvents, Authorization $authorization) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_UPDATE);
$valid = $validator->isValid($bucket->getUpdate());
$valid = $authorization->isValid(new Input(Database::PERMISSION_UPDATE, $bucket->getUpdate()));
if (!$fileSecurity && !$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}
// Read permission should not be required for update
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
$file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
@@ -1683,7 +1685,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
]);
// Users can only manage their own roles, API keys and Admin users can manage any
$roles = Authorization::getRoles();
$roles = $authorization->getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles) && !\is_null($permissions)) {
foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) {
@@ -1696,7 +1698,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
$permission->getIdentifier(),
$permission->getDimension()
))->toString();
if (!Authorization::isRole($role)) {
if (!$authorization->hasRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')');
}
}
@@ -1717,7 +1719,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
if ($fileSecurity && !$valid) {
$file = $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, $file);
} else {
$file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, $file));
$file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, $file));
}
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -1765,33 +1767,34 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
->inject('mode')
->inject('deviceForFiles')
->inject('queueForDeletes')
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Device $deviceForFiles, Delete $queueForDeletes) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
->inject('authorization')
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Device $deviceForFiles, Delete $queueForDeletes, Authorization $authorization) {
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_DELETE);
$valid = $validator->isValid($bucket->getDelete());
$valid = $authorization->isValid(new Input(Database::PERMISSION_DELETE, $bucket->getDelete()));
if (!$fileSecurity && !$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}
// Read permission should not be required for delete
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
$file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
// Make sure we don't delete the file before the document permission check occurs
if ($fileSecurity && !$valid && !$validator->isValid($file->getDelete())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
$validFile = $authorization->isValid(new Input(Database::PERMISSION_DELETE, $file->getDelete()));
if ($fileSecurity && !$valid && !$validFile) {
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}
$deviceDeleted = false;
@@ -1815,7 +1818,7 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
if ($fileSecurity && !$valid) {
$deleted = $dbForProject->deleteDocument('bucket_' . $bucket->getSequence(), $fileId);
} else {
$deleted = Authorization::skip(fn () => $dbForProject->deleteDocument('bucket_' . $bucket->getSequence(), $fileId));
$deleted = $authorization->skip(fn () => $dbForProject->deleteDocument('bucket_' . $bucket->getSequence(), $fileId));
}
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -1860,7 +1863,8 @@ App::get('/v1/storage/usage')
->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->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 = [];
@@ -1872,7 +1876,7 @@ App::get('/v1/storage/usage')
];
$total = [];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) {
$authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) {
foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
@@ -1950,7 +1954,8 @@ App::get('/v1/storage/:bucketId/usage')
->inject('project')
->inject('dbForProject')
->inject('getLogsDB')
->action(function (string $bucketId, string $range, Response $response, Document $project, Database $dbForProject, callable $getLogsDB) {
->inject('authorization')
->action(function (string $bucketId, string $range, Response $response, Document $project, Database $dbForProject, callable $getLogsDB, Authorization $authorization) {
$dbForLogs = call_user_func($getLogsDB, $project);
$bucket = $dbForProject->getDocument('buckets', $bucketId);
@@ -1968,7 +1973,7 @@ App::get('/v1/storage/:bucketId/usage')
str_replace('{bucketInternalId}', $bucket->getSequence(), METRIC_BUCKET_ID_FILES_IMAGES_TRANSFORMED),
];
Authorization::skip(function () use ($dbForProject, $dbForLogs, $bucket, $days, $metrics, &$stats) {
$authorization->skip(function () use ($dbForProject, $dbForLogs, $bucket, $days, $metrics, &$stats) {
foreach ($metrics as $metric) {
$db = ($metric === str_replace('{bucketInternalId}', $bucket->getSequence(), METRIC_BUCKET_ID_FILES_IMAGES_TRANSFORMED))
? $dbForLogs
+79 -68
View File
@@ -10,7 +10,7 @@ use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Email as EmailValidator;
use Appwrite\Network\Validator\Redirect;
use Appwrite\Platform\Workers\Deletes;
use Appwrite\SDK\AuthType;
@@ -28,9 +28,6 @@ use MaxMind\Db\Reader;
use Utopia\Abuse\Abuse;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Auth\Proofs\Password;
use Utopia\Auth\Proofs\Token;
use Utopia\Auth\Store;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@@ -51,10 +48,12 @@ use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Emails\Email;
use Utopia\Locale\Locale;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
@@ -85,16 +84,17 @@ 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 = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
$isAppUser = Auth::isAppUser($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)),
@@ -172,9 +172,10 @@ App::get('/v1/teams')
))
->param('queries', [], new Teams(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Teams::ALLOWED_ATTRIBUTES), true)
->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('response')
->inject('dbForProject')
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
->action(function (array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject) {
try {
$queries = Query::parseQueries($queries);
@@ -214,7 +215,7 @@ App::get('/v1/teams')
$filterQueries = Query::groupByType($queries)['filters'];
try {
$results = $dbForProject->find('teams', $queries);
$total = $dbForProject->count('teams', $filterQueries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('teams', $filterQueries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
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.");
}
@@ -469,14 +470,15 @@ App::post('/v1/teams/:teamId/memberships')
))
->label('abuse-limit', 10)
->param('teamId', '', new UID(), 'Team ID.')
->param('email', '', new Email(), 'Email of the new team member.', true)
->param('email', '', new EmailValidator(), 'Email of the new team member.', true)
->param('userId', '', new UID(), 'ID of the user to be added to a team.', true)
->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('roles', [], function (Document $project) {
if ($project->getId() === 'console') {
;
$roles = array_keys(Config::getParam('roles', []));
$roles = array_filter($roles, function ($role) {
return !in_array($role, [USER_ROLE_APPS, USER_ROLE_GUESTS, USER_ROLE_USERS]);
array_filter($roles, function ($role) {
return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]);
});
return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE);
}
@@ -488,6 +490,7 @@ App::post('/v1/teams/:teamId/memberships')
->inject('project')
->inject('user')
->inject('dbForProject')
->inject('authorization')
->inject('locale')
->inject('queueForMails')
->inject('queueForMessaging')
@@ -495,11 +498,9 @@ App::post('/v1/teams/:teamId/memberships')
->inject('timelimit')
->inject('queueForStatsUsage')
->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 = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(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) {
$isAppUser = Auth::isAppUser($authorization->getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
$url = htmlentities($url);
if (empty($url)) {
@@ -568,10 +569,15 @@ App::post('/v1/teams/:teamId/memberships')
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
}
try {
$userId = ID::unique();
$hash = $proofForPassword->hash($proofForPassword->generate());
$invitee = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([
$invitee = $authorization->skip(fn () => $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$permissions' => [
Permission::read(Role::any()),
@@ -584,9 +590,9 @@ App::post('/v1/teams/:teamId/memberships')
'emailVerification' => false,
'status' => true,
// TODO: Set password empty?
'password' => $hash,
'hash' => $proofForPassword->getHash()->getName(),
'hashOptions' => $proofForPassword->getHash()->getOptions(),
'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),
'hash' => Auth::DEFAULT_ALGO,
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
/**
* Set the password update time to 0 for users created using
* team invite and OAuth to allow password updates without an
@@ -601,13 +607,18 @@ App::post('/v1/teams/:teamId/memberships')
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $name]),
'emailCanonical' => $emailCanonical?->getCanonical(),
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
'emailIsCorporate' => $emailCanonical?->isCorporate(),
'emailIsDisposable' => $emailCanonical?->isDisposable(),
'emailIsFree' => $emailCanonical?->isFree(),
])));
} 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');
@@ -618,7 +629,7 @@ App::post('/v1/teams/:teamId/memberships')
Query::equal('teamInternalId', [$team->getSequence()]),
]);
$secret = $proofForToken->generate();
$secret = Auth::tokenGenerator();
if ($membership->isEmpty()) {
$membershipId = ID::unique();
$membership = new Document([
@@ -638,20 +649,20 @@ App::post('/v1/teams/:teamId/memberships')
'invited' => DateTime::now(),
'joined' => ($isPrivilegedUser || $isAppUser) ? DateTime::now() : null,
'confirm' => ($isPrivilegedUser || $isAppUser),
'secret' => $proofForToken->hash($secret),
'secret' => Auth::hash($secret),
'search' => implode(' ', [$membershipId, $invitee->getId()])
]);
$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));
$membership->setAttribute('secret', Auth::hash($secret));
$membership->setAttribute('invited', DateTime::now());
if ($isPrivilegedUser || $isAppUser) {
@@ -660,7 +671,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);
@@ -843,10 +854,12 @@ App::get('/v1/teams/:teamId/memberships')
->param('teamId', '', new UID(), 'Team ID.')
->param('queries', [], new Memberships(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Memberships::ALLOWED_ATTRIBUTES), true)
->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('response')
->inject('project')
->inject('dbForProject')
->action(function (string $teamId, array $queries, string $search, 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()) {
@@ -898,11 +911,11 @@ App::get('/v1/teams/:teamId/memberships')
collection: 'memberships',
queries: $queries,
);
$total = $dbForProject->count(
$total = $includeTotal ? $dbForProject->count(
collection: 'memberships',
queries: $filterQueries,
max: APP_LIMIT_COUNT
);
) : 0;
} catch (OrderException $e) {
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.");
}
@@ -916,7 +929,7 @@ App::get('/v1/teams/:teamId/memberships')
'mfa' => $project->getAttribute('auths', [])['membershipsMfa'] ?? true,
];
$roles = Authorization::getRoles();
$roles = $authorization->getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
@@ -987,7 +1000,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);
@@ -1007,7 +1021,7 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
'mfa' => $project->getAttribute('auths', [])['membershipsMfa'] ?? true,
];
$roles = Authorization::getRoles();
$roles = $authorization->getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
@@ -1075,7 +1089,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
if ($project->getId() === 'console') {
$roles = array_keys(Config::getParam('roles', []));
array_filter($roles, function ($role) {
return !in_array($role, [USER_ROLE_APPS, USER_ROLE_GUESTS, USER_ROLE_USERS]);
return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]);
});
return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE);
}
@@ -1086,8 +1100,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()) {
@@ -1104,9 +1119,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
throw new Exception(Exception::USER_NOT_FOUND);
}
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
$isAppUser = Auth::isAppUser($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
@@ -1187,12 +1202,11 @@ 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, Document $project, Reader $geodb, Event $queueForEvents) {
$protocol = $request->getProtocol();
$membership = $dbForProject->getDocument('memberships', $membershipId);
@@ -1201,7 +1215,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);
@@ -1211,7 +1225,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
throw new Exception(Exception::TEAM_MEMBERSHIP_MISMATCH);
}
if (!$proofForToken->verify($secret, $membership->getAttribute('secret'))) {
if (Auth::hash($secret) !== $membership->getAttribute('secret')) {
throw new Exception(Exception::TEAM_INVALID_SECRET);
}
@@ -1237,17 +1251,17 @@ 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());
$authDuration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::addSeconds(new \DateTime(), $authDuration);
$secret = $proofForToken->generate();
$secret = Auth::tokenGenerator();
$session = new Document(array_merge([
'$id' => ID::unique(),
'$permissions' => [
@@ -1257,9 +1271,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
],
'userId' => $user->getId(),
'userInternalId' => $user->getSequence(),
'provider' => SESSION_PROVIDER_EMAIL,
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => $user->getAttribute('email'),
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'factors' => ['email'],
@@ -1269,21 +1283,16 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
$session = $dbForProject->createDocument('sessions', $session);
Authorization::setRole(Role::user($userId)->toString());
$encoded = $store
->setProperty('id', $user->getId())
->setProperty('secret', $secret)
->encode();
$authorization->addRole(Role::user($userId)->toString());
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
$response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]));
}
$response
->addCookie(
name: $store->getKey() . '_legacy',
value: $encoded,
name: Auth::$cookieName . '_legacy',
value: Auth::encodeSession($user->getId(), $secret),
expire: (new \DateTime($expire))->getTimestamp(),
path: '/',
domain: Config::getParam('cookieDomain'),
@@ -1291,8 +1300,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
httponly: true
)
->addCookie(
name: $store->getKey(),
value: $encoded,
name: Auth::$cookieName,
value: Auth::encodeSession($user->getId(), $secret),
expire: (new \DateTime($expire))->getTimestamp(),
path: '/',
domain: Config::getParam('cookieDomain'),
@@ -1307,7 +1316,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())
@@ -1351,8 +1360,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);
@@ -1410,7 +1420,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
@@ -1442,11 +1452,12 @@ App::get('/v1/teams/:teamId/logs')
))
->param('teamId', '', new UID(), 'Team ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $teamId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->action(function (string $teamId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
$team = $dbForProject->getDocument('teams', $teamId);
@@ -1515,7 +1526,7 @@ App::get('/v1/teams/:teamId/logs')
}
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $includeTotal ? $audit->countLogsByResource($resource, $queries) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
+100 -129
View File
@@ -1,6 +1,7 @@
<?php
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Auth\MFA\Type;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Auth\Validator\Password;
@@ -15,7 +16,7 @@ use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Email as EmailValidator;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
@@ -31,18 +32,6 @@ use Appwrite\Utopia\Response;
use MaxMind\Db\Reader;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Auth\Hash;
use Utopia\Auth\Hashes\Argon2;
use Utopia\Auth\Hashes\Bcrypt;
use Utopia\Auth\Hashes\MD5;
use Utopia\Auth\Hashes\PHPass;
use Utopia\Auth\Hashes\Plaintext;
use Utopia\Auth\Hashes\Scrypt;
use Utopia\Auth\Hashes\ScryptModified;
use Utopia\Auth\Hashes\Sha;
use Utopia\Auth\Proofs\Password as ProofsPassword;
use Utopia\Auth\Proofs\Token;
use Utopia\Auth\Store;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@@ -60,20 +49,23 @@ use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Emails\Email;
use Utopia\Locale\Locale;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
/** TODO: Remove function when we move to using utopia/platform */
function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
{
$plaintextPassword = $password;
$hashOptionsObject = (\is_string($hashOptions)) ? \json_decode($hashOptions, true) : $hashOptions; // Cast to JSON array
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
if (!empty($email)) {
@@ -107,18 +99,13 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
}
}
$hashedPassword = null;
if (!empty($password)) {
if ($hash instanceof Plaintext) { // Password was never hashed, hash it with the default hash
$defaultHash = new ProofsPassword();
$hashedPassword = $defaultHash->hash($password);
$hash = $defaultHash->getHash();
} else {
$hashedPassword = $password;
}
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
}
$password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null;
$user = new Document([
'$id' => $userId,
'$permissions' => [
@@ -132,11 +119,11 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
'phoneVerification' => false,
'status' => true,
'labels' => [],
'password' => $hashedPassword,
'passwordHistory' => is_null($hashedPassword) || $passwordHistory === 0 ? [] : [$hashedPassword],
'passwordUpdate' => (!empty($hashedPassword)) ? DateTime::now() : null,
'hash' => $hash->getName(),
'hashOptions' => $hash->getOptions(),
'password' => $password,
'passwordHistory' => is_null($password) || $passwordHistory === 0 ? [] : [$password],
'passwordUpdate' => (!empty($password)) ? DateTime::now() : null,
'hash' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO : $hash,
'hashOptions' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO_OPTIONS : $hashOptionsObject + ['type' => $hash],
'registration' => DateTime::now(),
'reset' => false,
'name' => $name,
@@ -145,9 +132,14 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $phone, $name]),
'emailCanonical' => $emailCanonical?->getCanonical(),
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
'emailIsCorporate' => $emailCanonical?->isCorporate(),
'emailIsDisposable' => $emailCanonical?->isDisposable(),
'emailIsFree' => $emailCanonical?->isFree(),
]);
if ($hash instanceof Plaintext) {
if ($hash === 'plaintext') {
$hooks->trigger('passwordValidator', [$dbForProject, $project, $plaintextPassword, &$user, true]);
}
@@ -229,8 +221,8 @@ App::post('/v1/users')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', null, new Email(), 'User email.', true)
->param('phone', null, new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('email', null, new Nullable(new EmailValidator()), 'User email.', true)
->param('phone', null, new Nullable(new Phone()), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'Plain text user password. Must be at least 8 chars.', true, ['project', 'passwordsDictionary'])
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -238,9 +230,7 @@ App::post('/v1/users')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$plaintext = new Plaintext();
$user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks);
$user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($user, Response::MODEL_USER);
@@ -266,7 +256,7 @@ App::post('/v1/users/bcrypt')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Bcrypt.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -274,10 +264,7 @@ App::post('/v1/users/bcrypt')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$bcrypt = new Bcrypt();
$bcrypt->setCost(8); // Default cost
$user = createUser($bcrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -304,7 +291,7 @@ App::post('/v1/users/md5')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using MD5.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -312,9 +299,7 @@ App::post('/v1/users/md5')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$md5 = new MD5();
$user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser('md5', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -341,7 +326,7 @@ App::post('/v1/users/argon2')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Argon2.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -349,13 +334,7 @@ App::post('/v1/users/argon2')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$argon2 = new Argon2();
$argon2
->setMemoryCost(2048)
->setTimeCost(4)
->setThreads(3);
$user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -382,7 +361,7 @@ App::post('/v1/users/sha')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using SHA.')
->param('passwordVersion', '', new WhiteList(['sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512']), "Optional SHA version used to hash password. Allowed values are: 'sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512'", true)
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
@@ -391,12 +370,13 @@ App::post('/v1/users/sha')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$sha = new Sha();
$options = '{}';
if (!empty($passwordVersion)) {
$sha->setVersion($passwordVersion);
$options = '{"version":"' . $passwordVersion . '"}';
}
$user = createUser($sha, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser('sha', $options, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -423,7 +403,7 @@ App::post('/v1/users/phpass')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or pass the string `ID.unique()`to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using PHPass.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -431,9 +411,7 @@ App::post('/v1/users/phpass')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$phpass = new PHPass();
$user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -460,7 +438,7 @@ App::post('/v1/users/scrypt')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Scrypt.')
->param('passwordSalt', '', new Text(128), 'Optional salt used to hash password.')
->param('passwordCpu', 8, new Integer(), 'Optional CPU cost used to hash password.')
@@ -473,15 +451,15 @@ App::post('/v1/users/scrypt')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$scrypt = new Scrypt();
$scrypt
->setSalt($passwordSalt)
->setCpuCost($passwordCpu)
->setMemoryCost($passwordMemory)
->setParallelCost($passwordParallel)
->setLength($passwordLength);
$options = [
'salt' => $passwordSalt,
'costCpu' => $passwordCpu,
'costMemory' => $passwordMemory,
'costParallel' => $passwordParallel,
'length' => $passwordLength
];
$user = createUser($scrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -508,7 +486,7 @@ App::post('/v1/users/scrypt-modified')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Scrypt Modified.')
->param('passwordSalt', '', new Text(128), 'Salt used to hash password.')
->param('passwordSaltSeparator', '', new Text(128), 'Salt separator used to hash password.')
@@ -519,13 +497,7 @@ App::post('/v1/users/scrypt-modified')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$scryptModified = new ScryptModified();
$scryptModified
->setSalt($passwordSalt)
->setSaltSeparator($passwordSaltSeparator)
->setSignerKey($passwordSignerKey);
$user = createUser($scryptModified, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -568,7 +540,7 @@ App::post('/v1/users/:userId/targets')
switch ($providerType) {
case 'email':
$validator = new Email();
$validator = new EmailValidator();
if (!$validator->isValid($identifier)) {
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
@@ -646,9 +618,10 @@ App::get('/v1/users')
))
->param('queries', [], new Users(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Users::ALLOWED_ATTRIBUTES), true)
->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('response')
->inject('dbForProject')
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
->action(function (array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject) {
try {
$queries = Query::parseQueries($queries);
@@ -688,10 +661,10 @@ App::get('/v1/users')
$users = [];
$total = 0;
$dbForProject->skipFilters(function () use ($dbForProject, $queries, &$users, &$total) {
$dbForProject->skipFilters(function () use ($dbForProject, $queries, $includeTotal, &$users, &$total) {
try {
$users = $dbForProject->find('users', $queries);
$total = $dbForProject->count('users', $queries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('users', $queries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
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.");
} catch (QueryException $e) {
@@ -825,10 +798,11 @@ App::get('/v1/users/:userId/sessions')
]
))
->param('userId', '', new UID(), 'User ID.')
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->action(function (string $userId, Response $response, Database $dbForProject, Locale $locale) {
->action(function (string $userId, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale) {
$user = $dbForProject->getDocument('users', $userId);
@@ -850,7 +824,7 @@ App::get('/v1/users/:userId/sessions')
$response->dynamic(new Document([
'sessions' => $sessions,
'total' => count($sessions),
'total' => $includeTotal ? count($sessions) : 0,
]), Response::MODEL_SESSION_LIST);
});
@@ -874,9 +848,10 @@ App::get('/v1/users/:userId/memberships')
->param('userId', '', new UID(), 'User ID.')
->param('queries', [], new Memberships(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Memberships::ALLOWED_ATTRIBUTES), true)
->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('response')
->inject('dbForProject')
->action(function (string $userId, array $queries, string $search, Response $response, Database $dbForProject) {
->action(function (string $userId, array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
@@ -910,7 +885,7 @@ App::get('/v1/users/:userId/memberships')
$response->dynamic(new Document([
'memberships' => $memberships,
'total' => count($memberships),
'total' => $includeTotal ? count($memberships) : 0,
]), Response::MODEL_MEMBERSHIP_LIST);
});
@@ -933,11 +908,12 @@ App::get('/v1/users/:userId/logs')
))
->param('userId', '', new UID(), 'User ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $userId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->action(function (string $userId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
$user = $dbForProject->getDocument('users', $userId);
@@ -1006,7 +982,7 @@ App::get('/v1/users/:userId/logs')
}
$response->dynamic(new Document([
'total' => $audit->countLogsByUser($user->getSequence(), $queries),
'total' => $includeTotal ? $audit->countLogsByUser($user->getSequence(), $queries) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -1030,9 +1006,10 @@ App::get('/v1/users/:userId/targets')
))
->param('userId', '', new UID(), 'User ID.')
->param('queries', [], new Targets(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Targets::ALLOWED_ATTRIBUTES), true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForProject')
->action(function (string $userId, array $queries, Response $response, Database $dbForProject) {
->action(function (string $userId, array $queries, bool $includeTotal, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
@@ -1072,7 +1049,7 @@ App::get('/v1/users/:userId/targets')
}
try {
$targets = $dbForProject->find('targets', $queries);
$total = $dbForProject->count('targets', $queries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('targets', $queries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
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.");
}
@@ -1101,15 +1078,17 @@ App::get('/v1/users/identities')
))
->param('queries', [], new Identities(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Identities::ALLOWED_ATTRIBUTES), true)
->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('response')
->inject('dbForProject')
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
->action(function (array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
if (!empty($search)) {
$queries[] = Query::search('search', $search);
}
@@ -1139,10 +1118,9 @@ App::get('/v1/users/identities')
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
$identities = $dbForProject->find('identities', $queries);
$total = $dbForProject->count('identities', $filterQueries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('identities', $queries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
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.");
}
@@ -1376,21 +1354,12 @@ App::patch('/v1/users/:userId/password')
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
// Create Argon2 hasher with default settings
$hasher = new Argon2();
$hasher
->setMemoryCost(2048)
->setTimeCost(4)
->setThreads(3);
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
$newPassword = $hasher->hash($password);
$hash = ProofsPassword::createHash($user->getAttribute('hash'), $user->getAttribute('hashOptions'));
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
$history = $user->getAttribute('passwordHistory', []);
if ($historyLimit > 0) {
$validator = new PasswordHistory($history, $hash);
$validator = new PasswordHistory($history, $user->getAttribute('hash'), $user->getAttribute('hashOptions'));
if (!$validator->isValid($password)) {
throw new Exception(Exception::USER_PASSWORD_RECENTLY_USED);
}
@@ -1403,8 +1372,8 @@ App::patch('/v1/users/:userId/password')
->setAttribute('password', $newPassword)
->setAttribute('passwordHistory', $history)
->setAttribute('passwordUpdate', DateTime::now())
->setAttribute('hash', $hasher->getName())
->setAttribute('hashOptions', $hasher->getOptions());
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS);
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
@@ -1446,7 +1415,7 @@ App::patch('/v1/users/:userId/email')
]
))
->param('userId', '', new UID(), 'User ID.')
->param('email', '', new Email(allowEmpty: true), 'User email.')
->param('email', '', new EmailValidator(allowEmpty: true), 'User email.')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
@@ -1481,9 +1450,20 @@ App::patch('/v1/users/:userId/email')
$oldEmail = $user->getAttribute('email');
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
}
$user
->setAttribute('email', $email)
->setAttribute('emailVerification', false)
->setAttribute('emailCanonical', $emailCanonical?->getCanonical())
->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported())
->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate())
->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable())
->setAttribute('emailIsFree', $emailCanonical?->isFree())
;
try {
@@ -1744,7 +1724,7 @@ App::patch('/v1/users/:userId/targets/:targetId')
switch ($providerType) {
case 'email':
$validator = new Email();
$validator = new EmailValidator();
if (!$validator->isValid($identifier)) {
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
@@ -2217,19 +2197,17 @@ App::post('/v1/users/:userId/sessions')
->inject('locale')
->inject('geodb')
->inject('queueForEvents')
->inject('store')
->inject('proofForToken')
->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) {
->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$secret = $proofForToken->generate();
$secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION);
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$session = new Document(array_merge(
@@ -2237,8 +2215,8 @@ App::post('/v1/users/:userId/sessions')
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getSequence(),
'provider' => SESSION_PROVIDER_SERVER,
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
'provider' => Auth::SESSION_PROVIDER_SERVER,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'factors' => ['server'],
'ip' => $request->getIP(),
@@ -2262,13 +2240,8 @@ App::post('/v1/users/:userId/sessions')
$dbForProject->purgeCachedDocument('users', $user->getId());
$encoded = $store
->setProperty('id', $user->getId())
->setProperty('secret', $secret)
->encode();
$session
->setAttribute('secret', $encoded)
->setAttribute('secret', Auth::encodeSession($user->getId(), $secret))
->setAttribute('countryName', $countryName);
$queueForEvents
@@ -2303,7 +2276,7 @@ App::post('/v1/users/:userId/tokens')
))
->param('userId', '', new UID(), 'User ID.')
->param('length', 6, new Range(4, 128), 'Token length in characters. The default length is 6 characters', true)
->param('expire', TOKEN_EXPIRATION_GENERIC, new Range(60, TOKEN_EXPIRATION_LOGIN_LONG), 'Token expiration period in seconds. The default expiration is 15 minutes.', true)
->param('expire', Auth::TOKEN_EXPIRATION_GENERIC, new Range(60, Auth::TOKEN_EXPIRATION_LOGIN_LONG), 'Token expiration period in seconds. The default expiration is 15 minutes.', true)
->inject('request')
->inject('response')
->inject('dbForProject')
@@ -2315,17 +2288,15 @@ App::post('/v1/users/:userId/tokens')
throw new Exception(Exception::USER_NOT_FOUND);
}
$proofForToken = new Token($length);
$proofForToken->setHash(new Sha());
$secret = $proofForToken->generate();
$secret = Auth::tokenGenerator($length);
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $expire));
$token = new Document([
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getSequence(),
'type' => TOKEN_TYPE_GENERIC,
'secret' => $proofForToken->hash($secret),
'type' => Auth::TOKEN_TYPE_GENERIC,
'secret' => Auth::hash($secret),
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP()
@@ -2676,8 +2647,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 = [];
@@ -2687,7 +2658,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]),
+125 -45
View File
@@ -29,12 +29,20 @@ use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Detector\Detection\Framework\Analog;
use Utopia\Detector\Detection\Framework\Angular;
use Utopia\Detector\Detection\Framework\Astro;
use Utopia\Detector\Detection\Framework\Flutter;
use Utopia\Detector\Detection\Framework\Lynx;
use Utopia\Detector\Detection\Framework\NextJs;
use Utopia\Detector\Detection\Framework\Nuxt;
use Utopia\Detector\Detection\Framework\React;
use Utopia\Detector\Detection\Framework\ReactNative;
use Utopia\Detector\Detection\Framework\Remix;
use Utopia\Detector\Detection\Framework\Svelte;
use Utopia\Detector\Detection\Framework\SvelteKit;
use Utopia\Detector\Detection\Framework\TanStackStart;
use Utopia\Detector\Detection\Framework\Vue;
use Utopia\Detector\Detection\Packager\NPM;
use Utopia\Detector\Detection\Packager\PNPM;
use Utopia\Detector\Detection\Packager\Yarn;
@@ -58,11 +66,12 @@ use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\FileNotFound;
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, Request $request) {
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForPlatform, Authorization $authorization, Build $queueForBuilds, callable $getProjectDB, Request $request) {
$errors = [];
foreach ($repositories as $repository) {
try {
@@ -73,12 +82,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();
@@ -127,7 +136,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'),
@@ -166,7 +175,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
} finally {
$dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId);
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
} else {
@@ -177,7 +186,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))),
@@ -198,7 +207,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'),
@@ -237,7 +246,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
} finally {
$dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId);
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
}
@@ -280,7 +289,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()),
@@ -320,7 +329,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();
@@ -330,7 +339,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(),
@@ -363,7 +372,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(),
@@ -394,7 +403,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(),
@@ -446,7 +455,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', '')) : '';
@@ -458,7 +467,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment());
}
} finally {
$dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId);
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
}
@@ -818,7 +827,10 @@ App::post('/v1/vcs/github/installations/:installationId/detections')
$files = \array_column($files, 'name');
$languages = $github->listRepositoryLanguages($owner, $repositoryName);
$detector = new Packager($files);
$detector = new Packager();
foreach ($files as $file) {
$detector->addInput($file);
}
$detector
->addOption(new Yarn())
->addOption(new PNPM())
@@ -828,6 +840,14 @@ App::post('/v1/vcs/github/installations/:installationId/detections')
$packager = !\is_null($detection) ? $detection->getName() : 'npm';
if ($type === 'framework') {
$packages = '';
try {
$contentResponse = $github->getRepositoryContent($owner, $repositoryName, \rtrim($providerRootDirectory, '/') . '/package.json');
$packages = $contentResponse['content'] ?? '';
} catch (FileNotFound $e) {
// Continue detection without package.json
}
$output = new Document([
'framework' => '',
'installCommand' => '',
@@ -835,14 +855,27 @@ App::post('/v1/vcs/github/installations/:installationId/detections')
'outputDirectory' => '',
]);
$detector = new Framework($files, $packager);
$detector = new Framework($packager);
$detector->addInput($packages, Framework::INPUT_PACKAGES);
foreach ($files as $file) {
$detector->addInput($file, Framework::INPUT_FILE);
}
$detector
->addOption(new Flutter())
->addOption(new Nuxt())
->addOption(new Analog())
->addOption(new Angular())
->addOption(new Astro())
->addOption(new SvelteKit())
->addOption(new Flutter())
->addOption(new Lynx())
->addOption(new NextJs())
->addOption(new Remix());
->addOption(new Nuxt())
->addOption(new React())
->addOption(new ReactNative())
->addOption(new Remix())
->addOption(new Svelte())
->addOption(new SvelteKit())
->addOption(new TanStackStart())
->addOption(new Vue());
$framework = $detector->detect();
@@ -877,7 +910,18 @@ App::post('/v1/vcs/github/installations/:installationId/detections')
];
foreach ($strategies as $strategy) {
$detector = new Runtime($strategy === Strategy::LANGUAGES ? $languages : $files, $strategy, $packager);
$detector = new Runtime($strategy, $packager);
if ($strategy === Strategy::LANGUAGES) {
foreach ($languages as $language) {
$detector->addInput($language);
}
} else {
foreach ($files as $file) {
$detector->addInput($file);
}
}
$detector
->addOption(new Node())
->addOption(new Bun())
@@ -984,7 +1028,10 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
$files = $github->listRepositoryContents($repo['organization'], $repo['name'], '');
$files = \array_column($files, 'name');
$detector = new Packager($files);
$detector = new Packager();
foreach ($files as $file) {
$detector->addInput($file);
}
$detector
->addOption(new Yarn())
->addOption(new PNPM())
@@ -994,14 +1041,35 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
$packager = !\is_null($detection) ? $detection->getName() : 'npm';
if ($type === 'framework') {
$frameworkDetector = new Framework($files, $packager);
$packages = '';
try {
$contentResponse = $github->getRepositoryContent($repo['organization'], $repo['name'], 'package.json');
$packages = $contentResponse['content'] ?? '';
} catch (FileNotFound $e) {
// Continue detection without package.json
}
$frameworkDetector = new Framework($packager);
$frameworkDetector->addInput($packages, Framework::INPUT_PACKAGES);
foreach ($files as $file) {
$frameworkDetector->addInput($file, Framework::INPUT_FILE);
}
$frameworkDetector
->addOption(new Flutter())
->addOption(new Nuxt())
->addOption(new Analog())
->addOption(new Angular())
->addOption(new Astro())
->addOption(new SvelteKit())
->addOption(new Flutter())
->addOption(new Lynx())
->addOption(new NextJs())
->addOption(new Remix());
->addOption(new Nuxt())
->addOption(new React())
->addOption(new ReactNative())
->addOption(new Remix())
->addOption(new Svelte())
->addOption(new SvelteKit())
->addOption(new TanStackStart())
->addOption(new Vue());
$detectedFramework = $frameworkDetector->detect();
@@ -1026,7 +1094,16 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
];
foreach ($strategies as $strategy) {
$detector = new Runtime($strategy === Strategy::LANGUAGES ? $languages : $files, $strategy, $packager);
$detector = new Runtime($strategy, $packager);
if ($strategy === Strategy::LANGUAGES) {
foreach ($languages as $language) {
$detector->addInput($language);
}
} else {
foreach ($files as $file) {
$detector->addInput($file);
}
}
$detector
->addOption(new Node())
->addOption(new Bun())
@@ -1306,10 +1383,11 @@ App::post('/v1/vcs/github/events')
->inject('request')
->inject('response')
->inject('dbForPlatform')
->inject('authorization')
->inject('getProjectDB')
->inject('queueForBuilds')
->action(
function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, Authorization $authorization, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
$payload = $request->getRawPayload();
$signatureRemote = $request->getHeader('x-hub-signature-256', '');
$signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', '');
@@ -1345,14 +1423,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, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request);
}
} elseif ($event == $github::EVENT_INSTALLATION) {
if ($parsedPayload["action"] == "deleted") {
@@ -1365,16 +1443,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()));
}
$dbForPlatform->deleteDocument('installations', $installation->getId());
$authorization->skip(fn () => $dbForPlatform->deleteDocument('installations', $installation->getId()));
}
}
} elseif ($event == $github::EVENT_PULL_REQUEST) {
@@ -1403,12 +1481,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, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request);
} elseif ($parsedPayload["action"] == "closed") {
// Allowed external contributions cleanup
@@ -1417,7 +1495,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')
]));
@@ -1428,7 +1506,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));
}
}
}
@@ -1458,11 +1536,12 @@ App::get('/v1/vcs/installations')
))
->param('queries', [], new Installations(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Installations::ALLOWED_ATTRIBUTES), true)
->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('response')
->inject('project')
->inject('dbForProject')
->inject('dbForPlatform')
->action(function (array $queries, string $search, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform) {
->action(function (array $queries, string $search, bool $includeTotal, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@@ -1503,7 +1582,7 @@ App::get('/v1/vcs/installations')
$filterQueries = Query::groupByType($queries)['filters'];
try {
$results = $dbForPlatform->find('installations', $queries);
$total = $dbForPlatform->count('installations', $filterQueries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForPlatform->count('installations', $filterQueries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
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.");
}
@@ -1615,16 +1694,17 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
->inject('response')
->inject('project')
->inject('dbForPlatform')
->inject('authorization')
->inject('getProjectDB')
->inject('queueForBuilds')
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds) 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) use ($createGitDeployments) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$repository = Authorization::skip(fn () => $dbForPlatform->getDocument('repositories', $repositoryId, [
$repository = $authorization->skip(fn () => $dbForPlatform->getDocument('repositories', $repositoryId, [
Query::equal('projectInternalId', [$project->getSequence()])
]));
@@ -1641,7 +1721,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');
@@ -1665,7 +1745,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
$providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? '';
$providerCommitHash = $pullRequestResponse['head']['sha'] ?? '';
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, '', '', '', '', $providerCommitHash, '', '', '', '', $providerPullRequestId, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request);
$response->noContent();
});
+46 -48
View File
@@ -23,6 +23,7 @@ use Appwrite\Utopia\Request\Filters\V17 as RequestV17;
use Appwrite\Utopia\Request\Filters\V18 as RequestV18;
use Appwrite\Utopia\Request\Filters\V19 as RequestV19;
use Appwrite\Utopia\Request\Filters\V20 as RequestV20;
use Appwrite\Utopia\Request\Filters\V21 as RequestV21;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
@@ -56,7 +57,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, 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, string $previewHostname, Authorization $authorization, ?Key $apiKey)
{
$host = $request->getHostname() ?? '';
if (!empty($previewHostname)) {
@@ -65,9 +66,9 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($host)));
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', md5($host)));
} else {
$rule = Authorization::skip(
$rule = $authorization->skip(
fn () => $dbForPlatform->find('rules', [
Query::equal('domain', [$host]),
Query::limit(1)
@@ -108,7 +109,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)
);
@@ -116,7 +117,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));
}
/**
@@ -155,7 +156,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
@@ -169,15 +170,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') {
@@ -196,8 +197,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');
@@ -239,7 +240,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;
}
@@ -252,7 +253,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');
@@ -863,7 +864,8 @@ App::init()
->inject('apiKey')
->inject('httpReferrer')
->inject('httpReferrerSafe')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, array $platforms, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, Executor $executor, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, string $httpReferrer, string $httpReferrerSafe) {
->inject('authorization')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, array $platforms, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, Executor $executor, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, string $httpReferrer, string $httpReferrerSafe, Authorization $authorization) {
/*
* Appwrite Router
*/
@@ -871,7 +873,7 @@ App::init()
$mainDomain = System::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain
if ($host !== $mainDomain || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $authorization, $apiKey)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -906,6 +908,9 @@ App::init()
$dbForProject = $getProjectDB($project);
$request->addFilter(new RequestV20($dbForProject, $route->getPathValues($request)));
}
if (version_compare($requestFormat, '1.9.0', '<')) {
$request->addFilter(new RequestV21());
}
}
$domain = $request->getHostname();
@@ -919,7 +924,7 @@ App::init()
} elseif (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) {
Console::warning('Skipping SSL certificates generation on ACME challenge.');
} else {
Authorization::disable();
$authorization->disable();
$envDomain = System::getEnv('_APP_DOMAIN', '');
$mainDomain = null;
@@ -989,7 +994,7 @@ App::init()
}
$domains[$domain->get()] = true;
Authorization::reset(); // ensure authorization is re-enabled
$authorization->reset(); // ensure authorization is re-enabled
}
Config::setParam('domains', $domains);
}
@@ -1125,7 +1130,8 @@ App::options()
->inject('project')
->inject('devKey')
->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, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey) {
->inject('authorization')
->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, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Authorization $authorization) {
/*
* Appwrite Router
*/
@@ -1133,7 +1139,7 @@ App::options()
$mainDomain = System::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain
if ($host !== $mainDomain || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $authorization, $apiKey)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1174,7 +1180,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);
@@ -1185,29 +1192,17 @@ App::error()
$trace = $error->getTrace();
if (php_sapi_name() === 'cli') {
$logLevel = $code >= 500 || $code == 0 ? 'error' : 'warning';
$logPrefix = $code >= 500 || $code == 0 ? '[Error]' : '[Warning]';
Console::{$logLevel}($logPrefix . ' Timestamp: ' . date('c', time()));
Console::error('[Error] Timestamp: ' . date('c', time()));
if ($route) {
Console::{$logLevel}($logPrefix . ' Status Code: ' . $code);
Console::{$logLevel}($logPrefix . ' URL: ' . $route->getMethod() . ' ' . $route->getPath());
Console::error('[Error] Method: ' . $route->getMethod());
Console::error('[Error] URL: ' . $route->getPath());
}
Console::{$logLevel}($logPrefix . ' Type: ' . get_class($error));
Console::{$logLevel}($logPrefix . ' Message: ' . $message);
Console::{$logLevel}($logPrefix . ' File: ' . $file);
Console::{$logLevel}($logPrefix . ' Line: ' . $line);
Console::{$logLevel}($logPrefix . ' Trace:');
foreach ($trace as $index => $entry) {
$traceFile = $entry['file'] ?? 'unknown';
$traceLine = $entry['line'] ?? 0;
$traceFunction = $entry['function'] ?? 'unknown';
$traceClass = $entry['class'] ?? '';
$traceType = $entry['type'] ?? '';
Console::{$logLevel}(" #{$index} {$traceFile}({$traceLine}): {$traceClass}{$traceType}{$traceFunction}()");
}
Console::{$logLevel}('');
Console::error('[Error] Type: ' . get_class($error));
Console::error('[Error] Message: ' . $message);
Console::error('[Error] File: ' . $file);
Console::error('[Error] Line: ' . $line);
}
switch ($class) {
@@ -1268,7 +1263,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 (!Auth::isPrivilegedUser(Authorization::getRoles())) {
if (!Auth::isPrivilegedUser($authorization->getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
@@ -1330,7 +1325,7 @@ 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());
$action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD';
if (!empty($sdk)) {
@@ -1453,7 +1448,8 @@ App::get('/robots.txt')
->inject('isResourceBlocked')
->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, string $previewHostname, ?Key $apiKey) {
->inject('authorization')
->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, string $previewHostname, ?Key $apiKey, Authorization $authorization) {
$host = $request->getHostname() ?? '';
$consoleDomain = System::getEnv('_APP_CONSOLE_DOMAIN', '');
$mainDomain = System::getEnv('_APP_DOMAIN', '');
@@ -1462,7 +1458,7 @@ App::get('/robots.txt')
$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, $previewHostname, $apiKey)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $authorization, $apiKey)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1487,7 +1483,8 @@ App::get('/humans.txt')
->inject('isResourceBlocked')
->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, string $previewHostname, ?Key $apiKey) {
->inject('authorization')
->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, string $previewHostname, ?Key $apiKey, Authorization $authorization) {
$host = $request->getHostname() ?? '';
$consoleDomain = System::getEnv('_APP_CONSOLE_DOMAIN', '');
$mainDomain = System::getEnv('_APP_DOMAIN', '');
@@ -1496,7 +1493,7 @@ App::get('/humans.txt')
$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, $previewHostname, $apiKey)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $authorization, $apiKey)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1580,7 +1577,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);
}
@@ -1592,7 +1590,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);
});
+59 -96
View File
@@ -29,6 +29,7 @@ use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Authorization\Input;
use Utopia\Queue\Publisher;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
@@ -54,7 +55,20 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
};
if (array_key_exists($replace, $params)) {
$label = \str_replace($find, $params[$replace], $label);
$replacement = $params[$replace];
// Convert to string if it's not already a string
if (!is_string($replacement)) {
if (is_array($replacement)) {
$replacement = json_encode($replacement);
} elseif (is_object($replacement) && method_exists($replacement, '__toString')) {
$replacement = (string)$replacement;
} elseif (is_scalar($replacement)) {
$replacement = (string)$replacement;
} else {
throw new Exception(Exception::GENERAL_SERVER_ERROR, "The server encountered an error while parsing the label: $label. Please create an issue on GitHub to allow us to investigate further https://github.com/appwrite/appwrite/issues/new/choose");
}
}
$label = \str_replace($find, $replacement, $label);
}
}
return $label;
@@ -219,97 +233,45 @@ App::init()
->inject('mode')
->inject('team')
->inject('apiKey')
->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) {
->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();
/**
* Handle user authentication and session validation.
*
* This function follows a series of steps to determine the appropriate user session
* based on cookies, headers, and JWT tokens.
*
* Process:
*
* Project & Role Validation:
* 1. Check if the project is empty. If so, throw an exception.
* 2. Get the roles configuration.
* 3. Determine the role for the user based on the user document.
* 4. Get the scopes for the role.
*
* API Key Authentication:
* 5. If there is an API key:
* - Verify no user session exists simultaneously
* - Check if key is expired
* - Set role and scopes from API key
* - Handle special app role case
* - For standard keys, update last accessed time
*
* User Activity:
* 6. If the project is not the console and user is not admin:
* - Update user's last activity timestamp
*
* Access Control:
* 7. Get the method from the route
* 8. Validate namespace permissions
* 9. Validate scope permissions
* 10. Check if user is blocked
*
* Security Checks:
* 11. Verify password status (check if reset required)
* 12. Validate MFA requirements:
* - Check if MFA is enabled
* - Verify email status
* - Verify phone status
* - Verify authenticator status
* 13. Handle Multi-Factor Authentication:
* - Check remaining required factors
* - Validate factor completion
* - Throw exception if factors incomplete
*/
// Step 1: Check if project is empty
if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted' && str_starts_with($route->getPath(), '/v1/backups')) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Database Backups are available on Appwrite Cloud');
}
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
// Step 2: Get roles configuration
$roles = Config::getParam('roles', []);
// Step 3: Determine role for user
// TODO get scopes from the identity instead of the user roles config. The identity will containn the scopes the user authorized for the access token.
$role = $user->isEmpty()
? Role::guests()->toString()
: Role::users()->toString();
// Step 4: Get scopes for the role
$scopes = $roles[$role]['scopes'];
// Step 5: API Key Authentication
// 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);
}
// Set role and scopes from API key
$role = $apiKey->getRole();
$scopes = $apiKey->getScopes();
// Handle special app role case
if ($apiKey->getRole() === USER_ROLE_APPS) {
if ($apiKey->getRole() === Auth::USER_ROLE_APPS) {
// Disable authorization checks for API keys
Authorization::setDefaultStatus(false);
$authorization->setDefaultStatus(false);
$user = new Document([
'$id' => '',
'status' => true,
'type' => ACTIVITY_TYPE_APP,
'type' => Auth::ACTIVITY_TYPE_APP,
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => $apiKey->getName(),
@@ -318,7 +280,6 @@ App::init()
$queueForAudits->setUser($user);
}
// For standard keys, update last accessed time
if ($apiKey->getType() === API_KEY_STANDARD) {
$dbKey = $project->find(
key: 'secret',
@@ -378,25 +339,26 @@ 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 (Auth::getRoles($user) as $authRole) {
Authorization::setRole($authRole);
$authorization->addRole($role);
foreach (Auth::getRoles($user, $authorization) as $authRole) {
$authorization->addRole($authRole);
}
// Step 6: Update project and user last activity
// Update project last activity
if (!$project->isEmpty() && $project->getId() !== 'console') {
$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));
}
}
// Update user last activity
if (!empty($user->getId())) {
$accessedAt = $user->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
@@ -410,7 +372,6 @@ App::init()
}
}
// Steps 7-9: Access Control - Method, Namespace and Scope Validation
/**
* @var ?Method $method
*/
@@ -428,29 +389,27 @@ App::init()
if (
array_key_exists($namespace, $project->getAttribute('services', []))
&& !$project->getAttribute('services', [])[$namespace]
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
&& !(Auth::isPrivilegedUser($authorization->getRoles()) || Auth::isAppUser($authorization->getRoles()))
) {
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
}
}
// Step 9: Validate scope permissions
// Do now allow access if scope is not allowed
$allowed = (array)$route->getLabel('scope', 'none');
if (empty(\array_intersect($allowed, $scopes))) {
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, $user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scopes (' . \json_encode($allowed) . ')');
}
// Step 10: Check if user is blocked
// Do not allow access to blocked accounts
if (false === $user->getAttribute('status')) { // Account is blocked
throw new Exception(Exception::USER_BLOCKED);
}
// Step 11: Verify password status
if ($user->getAttribute('reset')) {
throw new Exception(Exception::USER_PASSWORD_RESET_REQUIRED);
}
// Step 12: Validate MFA requirements
$mfaEnabled = $user->getAttribute('mfa', false);
$hasVerifiedEmail = $user->getAttribute('emailVerification', false);
$hasVerifiedPhone = $user->getAttribute('phoneVerification', false);
@@ -458,7 +417,6 @@ App::init()
$hasMoreFactors = $hasVerifiedEmail || $hasVerifiedPhone || $hasVerifiedAuthenticator;
$minimumFactors = ($mfaEnabled && $hasMoreFactors) ? 2 : 1;
// Step 13: Handle Multi-Factor Authentication
if (!in_array('mfa', $route->getGroups())) {
if ($session && \count($session->getAttribute('factors', [])) < $minimumFactors) {
throw new Exception(Exception::USER_MORE_FACTORS_REQUIRED);
@@ -491,14 +449,15 @@ App::init()
->inject('plan')
->inject('devKey')
->inject('telemetry')
->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, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry) use ($usageDatabaseListener, $eventDatabaseListener) {
->inject('authorization')
->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, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, Authorization $authorization) use ($usageDatabaseListener, $eventDatabaseListener) {
$route = $utopia->getRoute();
if (
array_key_exists('rest', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['rest']
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
&& !(Auth::isPrivilegedUser($authorization->getRoles()) || Auth::isAppUser($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
@@ -528,7 +487,7 @@ App::init()
$closestLimit = null;
$roles = Authorization::getRoles();
$roles = $authorization->getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
@@ -585,7 +544,7 @@ App::init()
if (!$user->isEmpty()) {
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
$userClone->setAttribute('type', Auth::ACTIVITY_TYPE_USER);
$queueForAudits->setUser($userClone);
}
@@ -628,10 +587,10 @@ App::init()
if ($useCache) {
$route = $utopia->match($request);
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !Auth::isPrivilegedUser(Authorization::getRoles());
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !Auth::isPrivilegedUser($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())
);
@@ -639,23 +598,26 @@ App::init()
$data = $cache->load($key, $timestamp);
if (!empty($data) && !$cacheLog->isEmpty()) {
$usageMetric = $route->getLabel('usage.metric', null);
if ($usageMetric === METRIC_AVATARS_SCREENSHOTS_GENERATED) {
$queueForStatsUsage->disableMetric(METRIC_AVATARS_SCREENSHOTS_GENERATED);
}
$parts = explode('/', $cacheLog->getAttribute('resourceType', ''));
$type = $parts[0] ?? null;
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 = Auth::isPrivilegedUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAppUser && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$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);
}
@@ -666,7 +628,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()) {
@@ -677,11 +639,11 @@ App::init()
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
//Do not update transformedAt if it's a console user
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
if (!Auth::isPrivilegedUser($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));
}
}
}
@@ -777,7 +739,8 @@ App::shutdown()
->inject('queueForWebhooks')
->inject('queueForRealtime')
->inject('dbForProject')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) use ($parseLabel) {
->inject('authorization')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $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) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -825,7 +788,7 @@ App::shutdown()
if (!$user->isEmpty()) {
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
$userClone->setAttribute('type', Auth::ACTIVITY_TYPE_USER);
$queueForAudits->setUser($userClone);
} elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) {
/**
@@ -839,7 +802,7 @@ App::shutdown()
$user = new Document([
'$id' => '',
'status' => true,
'type' => ACTIVITY_TYPE_GUEST,
'type' => Auth::ACTIVITY_TYPE_GUEST,
'email' => 'guest.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => 'Guest',
@@ -903,11 +866,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,
@@ -917,7 +880,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']);
}
@@ -929,7 +892,7 @@ App::shutdown()
}
if ($project->getId() !== 'console') {
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
if (!Auth::isPrivilegedUser($authorization->getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
+5 -4
View File
@@ -20,7 +20,7 @@ App::init()
$lastUpdate = $session->getAttribute('mfaUpdatedAt');
if (!empty($lastUpdate)) {
$now = DateTime::now();
$maxAllowedDate = DateTime::addSeconds(new \DateTime($lastUpdate), MFA_RECENT_DURATION); // Maximum date until session is considered safe before asking for another challenge
$maxAllowedDate = DateTime::addSeconds(new \DateTime($lastUpdate), Auth::MFA_RECENT_DURATION); // Maximum date until session is considered safe before asking for another challenge
$isSessionFresh = DateTime::formatTz($maxAllowedDate) >= DateTime::formatTz($now);
}
@@ -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 = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles());
$isAppUser = Auth::isAppUser($authorization->getRoles());
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
return;
+17 -11
View File
@@ -25,7 +25,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;
@@ -259,7 +258,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(Audit::COLLECTION)->isEmpty()) {
$audit = new Audit($dbForPlatform);
$audit->setup();
@@ -318,9 +319,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',
@@ -335,7 +336,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'] ?? [];
@@ -363,7 +364,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));
}
});
@@ -454,8 +455,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) {
@@ -497,7 +502,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);
@@ -556,7 +561,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;
@@ -573,7 +578,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());
}
+5 -66
View File
@@ -90,74 +90,9 @@ const APP_PLATFORM_CLIENT = 'client';
const APP_PLATFORM_CONSOLE = 'console';
const APP_VCS_GITHUB_USERNAME = 'Appwrite';
const APP_VCS_GITHUB_EMAIL = 'team@appwrite.io';
const APP_VCS_GITHUB_URL = 'https://github.com/TeamAppwrite';
const APP_BRANDED_EMAIL_BASE_TEMPLATE = 'email-base-styled';
// User Roles
const USER_ROLE_ANY = 'any';
const USER_ROLE_GUESTS = 'guests';
const USER_ROLE_USERS = 'users';
const USER_ROLE_ADMIN = 'admin';
const USER_ROLE_DEVELOPER = 'developer';
const USER_ROLE_OWNER = 'owner';
const USER_ROLE_APPS = 'apps';
const USER_ROLE_SYSTEM = 'system';
/**
* Token Expiration times.
*/
const TOKEN_EXPIRATION_LOGIN_LONG = 31536000; /* 1 year */
const TOKEN_EXPIRATION_LOGIN_SHORT = 3600; /* 1 hour */
const TOKEN_EXPIRATION_RECOVERY = 3600; /* 1 hour */
const TOKEN_EXPIRATION_CONFIRM = 3600 * 1; /* 1 hour */
const TOKEN_EXPIRATION_OTP = 60 * 15; /* 15 minutes */
const TOKEN_EXPIRATION_GENERIC = 60 * 15; /* 15 minutes */
/**
* Token Lengths.
*/
const TOKEN_LENGTH_MAGIC_URL = 64;
const TOKEN_LENGTH_VERIFICATION = 256;
const TOKEN_LENGTH_RECOVERY = 256;
const TOKEN_LENGTH_OAUTH2 = 64;
const TOKEN_LENGTH_SESSION = 256;
/**
* Token Types.
*/
const TOKEN_TYPE_LOGIN = 1; // Deprecated
const TOKEN_TYPE_VERIFICATION = 2;
const TOKEN_TYPE_RECOVERY = 3;
const TOKEN_TYPE_INVITE = 4;
const TOKEN_TYPE_MAGIC_URL = 5;
const TOKEN_TYPE_PHONE = 6;
const TOKEN_TYPE_OAUTH2 = 7;
const TOKEN_TYPE_GENERIC = 8;
const TOKEN_TYPE_EMAIL = 9; // OTP
/**
* Session Providers.
*/
const SESSION_PROVIDER_EMAIL = 'email';
const SESSION_PROVIDER_ANONYMOUS = 'anonymous';
const SESSION_PROVIDER_MAGIC_URL = 'magic-url';
const SESSION_PROVIDER_PHONE = 'phone';
const SESSION_PROVIDER_OAUTH2 = 'oauth2';
const SESSION_PROVIDER_TOKEN = 'token';
const SESSION_PROVIDER_SERVER = 'server';
/**
* Activity associated with user or the app.
*/
const ACTIVITY_TYPE_APP = 'app';
const ACTIVITY_TYPE_USER = 'user';
const ACTIVITY_TYPE_GUEST = 'guest';
/**
* MFA
*/
const MFA_RECENT_DURATION = 1800; // 30 mins
// Database Reconnect
const DATABASE_RECONNECT_SLEEP = 2;
const DATABASE_RECONNECT_MAX_ATTEMPTS = 10;
@@ -203,6 +138,7 @@ const DELETE_TYPE_TOPIC = 'topic';
const DELETE_TYPE_TARGET = 'target';
const DELETE_TYPE_EXPIRED_TARGETS = 'invalid_targets';
const DELETE_TYPE_SESSION_TARGETS = 'session_targets';
const DELETE_TYPE_CSV_EXPORTS = 'csv_exports';
const DELETE_TYPE_MAINTENANCE = 'maintenance';
// Message types
@@ -335,6 +271,9 @@ const METRIC_SITES_OUTBOUND = 'sites.outbound';
const METRIC_SITES_ID_REQUESTS = 'sites.{siteInternalId}.requests';
const METRIC_SITES_ID_INBOUND = 'sites.{siteInternalId}.inbound';
const METRIC_SITES_ID_OUTBOUND = 'sites.{siteInternalId}.outbound';
const METRIC_AVATARS_SCREENSHOTS_GENERATED = 'avatars.screenshotsGenerated';
const METRIC_FUNCTIONS_RUNTIME = 'functions.runtimes.{runtime}';
const METRIC_SITES_FRAMEWORK = 'sites.frameworks.{framework}';
// Resource types
const RESOURCE_TYPE_PROJECTS = 'projects';
+7 -8
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(
@@ -176,7 +175,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),
]));
@@ -189,7 +188,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),
@@ -203,7 +202,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),
@@ -217,7 +216,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),
@@ -231,7 +230,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),
@@ -331,7 +330,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)
@@ -345,7 +344,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()]),
+79 -128
View File
@@ -24,16 +24,9 @@ use Appwrite\GraphQL\Schema;
use Appwrite\Network\Platform;
use Appwrite\Network\Validator\Origin;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Executor\Executor;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\App;
use Utopia\Auth\Hashes\Argon2;
use Utopia\Auth\Hashes\Sha;
use Utopia\Auth\Proofs\Code;
use Utopia\Auth\Proofs\Password;
use Utopia\Auth\Proofs\Token;
use Utopia\Auth\Store;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
@@ -159,7 +152,7 @@ App::setResource('queueForMigrations', function (Publisher $publisher) {
App::setResource('queueForStatsResources', function (Publisher $publisher) {
return new StatsResources($publisher);
}, ['publisher']);
App::setResource('platforms', function (Request $request, Document $console, Document $project, Database $dbForPlatform) {
App::setResource('platforms', function (Request $request, Document $console, Document $project, Database $dbForPlatform, Authorization $authorization) {
$console->setAttribute('platforms', [ // Always allow current host
'$collection' => ID::custom('platforms'),
'name' => 'Current Host',
@@ -207,9 +200,9 @@ App::setResource('platforms', function (Request $request, Document $console, Doc
// Safe if rule with same project ID exists
if (!empty($origin)) {
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin ?? '')));
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', md5($origin ?? '')));
} else {
$rule = Authorization::skip(
$rule = $authorization->skip(
fn () => $dbForPlatform->find('rules', [
Query::equal('domain', [$origin]),
Query::limit(1)
@@ -231,102 +224,82 @@ App::setResource('platforms', function (Request $request, Document $console, Doc
...$console->getAttribute('platforms', []),
...$project->getAttribute('platforms', []),
];
}, ['request', 'console', 'project', 'dbForPlatform']);
}, ['request', 'console', 'project', 'dbForPlatform', 'authorization']);
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 ($mode, $project, $console, $request, $response, $dbForProject, $dbForPlatform, $authorization) {
/** @var Appwrite\Utopia\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Database\Database $dbForPlatform */
/** @var Utopia\Database\Authorization $authorization */
/** @var string $mode */
/** @var Utopia\Auth\Store $store */
/**
* Handles user authentication and session validation.
*
* This function follows a series of steps to determine the appropriate user session
* based on cookies, headers, and JWT tokens.
*
* Process:
* 1. Checks the cookie based on mode:
* - If in admin mode, uses console project id for key.
* - Otherwise, sets the key using the project ID
* 2. If no cookie is found, attempts to retrieve the fallback header `x-fallback-cookies`.
* - If this method is used, returns the header: `X-Debug-Fallback: true`.
* 3. Fetches the user document from the appropriate database based on the mode.
* 4. If the user document is empty or the session key cannot be verified, sets an empty user document.
* 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.
*/
$authorization->setDefaultStatus(true);
Authorization::setDefaultStatus(true);
$store->setKey('a_session_' . $project->getId());
Auth::setCookieName('a_session_' . $project->getId());
if (APP_MODE_ADMIN === $mode) {
$store->setKey('a_session_' . $console->getId());
Auth::setCookieName('a_session_' . $console->getId());
}
$store->decode(
$session = Auth::decodeSession(
$request->getCookie(
$store->getKey(), // Get sessions
$request->getCookie($store->getKey() . '_legacy', '')
Auth::$cookieName, // Get sessions
$request->getCookie(Auth::$cookieName . '_legacy', '')
)
);
// Get session from header for SSR clients
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
if (empty($session['id']) && empty($session['secret'])) {
$sessionHeader = $request->getHeader('x-appwrite-session', '');
if (!empty($sessionHeader)) {
$store->decode($sessionHeader);
$session = Auth::decodeSession($sessionHeader);
}
}
// Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies
if ($response) { // if in http context - add debug header
if ($response) {
$response->addHeader('X-Debug-Fallback', 'false');
}
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
if (empty($session['id']) && empty($session['secret'])) {
if ($response) {
$response->addHeader('X-Debug-Fallback', 'true');
}
$fallback = $request->getHeader('x-fallback-cookies', '');
$fallback = \json_decode($fallback, true);
$store->decode(((is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : ''));
$session = Auth::decodeSession(((isset($fallback[Auth::$cookieName])) ? $fallback[Auth::$cookieName] : ''));
}
Auth::$unique = $session['id'] ?? '';
Auth::$secret = $session['secret'] ?? '';
$user = new Document([]);
if (APP_MODE_ADMIN === $mode) {
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
} else {
if ($project->isEmpty()) {
$user = new Document([]);
} else {
if (!empty($store->getProperty('id', ''))) {
if ($project->getId() === 'console') {
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
} else {
$user = $dbForProject->getDocument('users', $store->getProperty('id', ''));
}
if (!empty(Auth::$unique)) {
if ($mode === APP_MODE_ADMIN) {
$user = $dbForPlatform->getDocument('users', Auth::$unique);
} elseif (!$project->isEmpty()) {
if ($project->getId() === 'console') {
$user = $dbForPlatform->getDocument('users', Auth::$unique);
} else {
$user = $dbForProject->getDocument('users', Auth::$unique);
}
}
}
if (
$user->isEmpty() // Check a document has been found in the DB
|| !Auth::sessionVerify($user->getAttribute('sessions', []), $store->getProperty('secret', ''), $proofForToken)
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret)
) { // Validate user has valid login token
$user = new Document([]);
}
// 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([]);
// }
@@ -364,9 +337,9 @@ App::setResource('user', function (string $mode, Document $project, Document $co
$dbForPlatform->setMetadata('user', $user->getId());
return $user;
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken']);
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', '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 */
@@ -377,18 +350,18 @@ 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 (Document $user, Store $store, Token $proofForToken) {
App::setResource('session', function (Document $user) {
if ($user->isEmpty()) {
return;
}
$sessions = $user->getAttribute('sessions', []);
$sessionId = Auth::sessionVerify($user->getAttribute('sessions'), $store->getProperty('secret', ''), $proofForToken);
$sessionId = Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret);
if (!$sessionId) {
return;
@@ -401,13 +374,17 @@ App::setResource('session', function (Document $user, Store $store, Token $proof
}
return;
}, ['user', 'store', 'proofForToken']);
}, ['user']);
App::setResource('console', function () {
return new Document(Config::getParam('console'));
}, []);
App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project) {
App::setResource('authorization', function () {
return new Authorization();
}, []);
App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Authorization $authorization) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -423,6 +400,7 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform
$database = new Database($adapter, $cache);
$database
->setAuthorization($authorization)
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId())
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
@@ -443,13 +421,15 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform
}
return $database;
}, ['pools', 'dbForPlatform', 'cache', 'project']);
}, ['pools', 'dbForPlatform', 'cache', 'project', '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
->setAuthorization($authorization)
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', 'console')
@@ -457,12 +437,12 @@ App::setResource('dbForPlatform', function (Group $pools, Cache $cache) {
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
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;
}
@@ -474,8 +454,9 @@ 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
->setAuthorization($authorization)
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId())
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
@@ -509,12 +490,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;
@@ -524,6 +505,7 @@ App::setResource('getLogsDB', function (Group $pools, Cache $cache) {
$database = new Database($adapter, $cache);
$database
->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
@@ -536,7 +518,7 @@ App::setResource('getLogsDB', function (Group $pools, Cache $cache) {
return $database;
};
}, ['pools', 'cache']);
}, ['pools', 'cache', 'authorization']);
App::setResource('telemetry', fn () => new NoTelemetry());
@@ -583,7 +565,7 @@ App::setResource('deviceForFiles', function ($project, Telemetry $telemetry) {
App::setResource('deviceForSites', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
App::setResource('deviceForImports', function ($project, Telemetry $telemetry) {
App::setResource('deviceForMigrations', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
App::setResource('deviceForFunctions', function ($project, Telemetry $telemetry) {
@@ -730,7 +712,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'] ?? []);
@@ -740,8 +722,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),
]));
@@ -815,7 +797,7 @@ App::setResource('schema', function ($utopia, $dbForProject) {
$urls,
$params,
);
}, ['utopia', 'dbForProject']);
}, ['utopia', 'dbForProject', 'authorization']);
App::setResource('contributors', function () {
$path = 'app/config/contributors.json';
@@ -861,7 +843,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
@@ -880,7 +862,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());
}
@@ -897,14 +879,14 @@ 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', '');
@@ -914,7 +896,7 @@ App::setResource('team', function (Document $project, Database $dbForPlatform, A
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', '');
@@ -923,7 +905,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;
}
}
@@ -932,14 +914,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',
@@ -975,40 +957,9 @@ App::setResource('apiKey', function (Request $request, Document $project): ?Key
return Key::decode($project, $key);
}, ['request', 'project']);
App::setResource('store', function (): Store {
return new Store();
});
App::setResource('proofForPassword', function (): Password {
$hash = new Argon2();
$hash
->setMemoryCost(2048)
->setTimeCost(4)
->setThreads(3);
$password = new Password();
$password
->setHash($hash);
return $password;
});
App::setResource('proofForToken', function (): Token {
$token = new Token();
$token->setHash(new Sha());
return $token;
});
App::setResource('proofForCode', function (): Code {
$code = new Code();
$code->setHash(new Sha());
return $code;
});
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
@@ -1025,7 +976,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([]);
@@ -1043,7 +994,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'));
@@ -1054,7 +1005,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([
@@ -1069,7 +1020,7 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
};
}
return new Document([]);
}, ['project', 'dbForProject', 'request']);
}, ['project', 'dbForProject', 'request', 'authorization']);
App::setResource('httpReferrer', function (Request $request): string {
$referrer = $request->getReferer();
@@ -1102,6 +1053,6 @@ App::setResource('httpReferrerSafe', function (Request $request, string $httpRef
return $referrer;
}, ['request', 'httpReferrer', 'platforms', 'dbForPlatform', 'project', 'utopia']);
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']);
+16 -29
View File
@@ -16,9 +16,6 @@ use Swoole\Timer;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\App;
use Utopia\Auth\Hashes\Sha;
use Utopia\Auth\Proofs\Token;
use Utopia\Auth\Store;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
@@ -31,7 +28,6 @@ use Utopia\Database\Document;
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;
@@ -302,7 +298,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})...");
@@ -332,7 +328,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");
}
@@ -363,7 +359,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)),
]));
@@ -457,12 +453,11 @@ $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);
$user = $database->getDocument('users', $userId);
$roles = Auth::getRoles($user);
$roles = Auth::getRoles($user, $database->getAuthorization());
$channels = $realtime->connections[$connection]['channels'];
$realtime->unsubscribe($connection);
@@ -518,6 +513,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
try {
/** @var Document $project */
$project = $app->getResource('project');
$authorization = $app->getResource('authorization');
/*
* Project Check
@@ -529,7 +525,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
if (
array_key_exists('realtime', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['realtime']
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
&& !(Auth::isPrivilegedUser($authorization->getRoles()) || Auth::isAppUser($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
@@ -566,7 +562,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
}
$roles = Auth::getRoles($user);
$roles = Auth::getRoles($user, $authorization);
$channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId());
@@ -640,8 +636,8 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$database = getConsoleDB();
if ($projectId !== 'console') {
$project = Authorization::skip(fn () => $database->getDocument('projects', $projectId));
$database = getProjectDB($project);
$project = $database->getAuthorization()->skip(fn () => $database->getDocument('projects', $projectId));
$database = getProjectDB($project, $database->getAuthorization());
} else {
$project = null;
}
@@ -681,30 +677,21 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
}
$store = new Store();
$session = Auth::decodeSession($message['data']['session']);
Auth::$unique = $session['id'] ?? '';
Auth::$secret = $session['secret'] ?? '';
$store->decode($message['data']['session']);
$user = $database->getDocument('users', $store->getProperty('id', ''));
/**
* TODO:
* Moving forward, we should try to use our dependency injection container
* to inject the proof for token.
* This way we will have one source of truth for the proof for token.
*/
$proofForToken = new Token();
$proofForToken->setHash(new Sha());
$user = $database->getDocument('users', Auth::$unique);
if (
empty($user->getId()) // Check a document has been found in the DB
|| !Auth::sessionVerify($user->getAttribute('sessions', []), $store->getProperty('secret', ''), $proofForToken) // Validate user has valid login token
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret) // Validate user has valid login token
) {
// cookie not valid
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Session is not valid.');
}
$roles = Auth::getRoles($user);
$roles = Auth::getRoles($user, $database->getAuthorization());
$channels = Realtime::convertChannels(array_flip($realtime->connections[$connection]['channels']), $user->getId());
$realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels);
+2 -2
View File
@@ -179,7 +179,7 @@ $image = $this->getParam('image', '');
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: <?php echo $organization; ?>/console:7.0.2
image: <?php echo $organization; ?>/console:7.4.7
restart: unless-stopped
networks:
- appwrite
@@ -859,7 +859,7 @@ $image = $this->getParam('image', '');
- _APP_ASSISTANT_OPENAI_API_KEY
appwrite-browser:
image: appwrite/browser:0.2.4
image: appwrite/browser:0.3.1
container_name: appwrite-browser
<<: *x-logging
restart: unless-stopped
+33 -18
View File
@@ -45,19 +45,28 @@ 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
->setAuthorization($authorization)
->setNamespace('_console');
return $dbForPlatform;
}, ['cache', 'register']);
}, ['cache', 'register', 'authorization']);
Server::setResource('project', function (Message $message, Database $dbForPlatform) {
$payload = $message->getPayload() ?? [];
@@ -70,7 +79,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;
}
@@ -101,15 +110,17 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
->setNamespace('_' . $project->getSequence());
}
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
$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;
}
@@ -123,7 +134,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)) {
@@ -160,15 +171,17 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
->setNamespace('_' . $project->getSequence());
}
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
$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;
@@ -178,6 +191,7 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache) {
$database = new Database($adapter, $cache);
$database
->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
@@ -190,7 +204,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
@@ -349,7 +363,7 @@ Server::setResource('deviceForSites', function (Document $project, Telemetry $te
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForImports', function (Document $project, Telemetry $telemetry) {
Server::setResource('deviceForMigrations', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
@@ -478,7 +492,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) {
@@ -494,7 +509,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);
+7 -8
View File
@@ -1,5 +1,4 @@
{
"name": "appwrite/server-ce",
"description": "End to end backend server for frontend and mobile apps.",
"type": "project",
@@ -46,24 +45,24 @@
"ext-sockets": "*",
"appwrite/php-runtimes": "0.19.*",
"appwrite/php-clamav": "2.0.*",
"utopia-php/auth": "0.4.*",
"utopia-php/abuse": "1.*",
"utopia-php/analytics": "0.10.*",
"utopia-php/audit": "1.*",
"utopia-php/cache": "0.13.*",
"utopia-php/cli": "0.15.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "2.*",
"utopia-php/detector": "0.1.*",
"utopia-php/domains": "0.8.*",
"utopia-php/dns": "0.3.*",
"utopia-php/database": "4.*",
"utopia-php/detector": "0.2.*",
"utopia-php/domains": "0.9.*",
"utopia-php/emails": "0.6.*",
"utopia-php/dns": "1.1.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/framework": "0.33.*",
"utopia-php/fetch": "0.4.*",
"utopia-php/image": "0.8.*",
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.19.*",
"utopia-php/messaging": "0.20.*",
"utopia-php/migration": "1.*",
"utopia-php/orchestration": "0.9.*",
"utopia-php/platform": "0.7.*",
@@ -75,7 +74,7 @@
"utopia-php/swoole": "0.8.*",
"utopia-php/system": "0.9.*",
"utopia-php/telemetry": "0.1.*",
"utopia-php/vcs": "0.11.*",
"utopia-php/vcs": "0.12.*",
"utopia-php/websocket": "0.3.*",
"matomo/device-detector": "6.4.*",
"dragonmantank/cron-expression": "3.4.*",
Generated
+550 -226
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
services:
appwrite:
# volumes:
# - ~/.ssh:/root/.ssh
environment:
- GH_TOKEN=
- GIT_EMAIL=
+4 -3
View File
@@ -219,7 +219,7 @@ services:
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: appwrite/console:7.0.2
image: appwrite/console:7.4.11
restart: unless-stopped
networks:
- appwrite
@@ -698,6 +698,7 @@ services:
- appwrite
volumes:
- appwrite-imports:/storage/imports:rw
- appwrite-uploads:/storage/uploads:rw
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
- ./tests:/usr/src/code/tests
@@ -957,7 +958,7 @@ services:
appwrite-browser:
container_name: appwrite-browser
image: appwrite/browser:0.2.4
image: appwrite/browser:0.3.1
networks:
- appwrite
@@ -966,7 +967,7 @@ services:
hostname: exc1
<<: *x-logging
stop_signal: SIGINT
image: openruntimes/executor:0.11.0
image: openruntimes/executor:0.11.4
restart: unless-stopped
networks:
- appwrite
@@ -10,6 +10,7 @@ Account account = new Account(client);
account.listIdentities(
listOf(), // queries (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -10,6 +10,7 @@ Account account = new Account(client);
account.listLogs(
listOf(), // queries (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -0,0 +1,41 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Avatars;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
.setProject("<YOUR_PROJECT_ID>"); // Your project ID
Avatars avatars = new Avatars(client);
avatars.getScreenshot(
"https://example.com", // url
mapOf( "a" to "b" ), // headers (optional)
1, // viewportWidth (optional)
1, // viewportHeight (optional)
0.1, // scale (optional)
theme.LIGHT, // theme (optional)
"<USER_AGENT>", // userAgent (optional)
false, // fullpage (optional)
"<LOCALE>", // locale (optional)
timezone.AFRICA_ABIDJAN, // timezone (optional)
-90, // latitude (optional)
-180, // longitude (optional)
0, // accuracy (optional)
false, // touch (optional)
listOf(), // permissions (optional)
0, // sleep (optional)
0, // width (optional)
0, // height (optional)
-1, // quality (optional)
output.JPG, // output (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
return;
}
Log.d("Appwrite", result.toString());
})
);
@@ -1,6 +1,8 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Databases;
import io.appwrite.Permission;
import io.appwrite.Role;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -19,7 +21,7 @@ databases.createDocument(
"age" to 30,
"isAdmin" to false
), // data
listOf("read("any")"), // permissions (optional)
listOf(Permission.read(Role.any())), // permissions (optional)
"<TRANSACTION_ID>", // transactionId (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -13,6 +13,7 @@ databases.listDocuments(
"<COLLECTION_ID>", // collectionId
listOf(), // queries (optional)
"<TRANSACTION_ID>", // transactionId (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -1,6 +1,8 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Databases;
import io.appwrite.Permission;
import io.appwrite.Role;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -13,7 +15,7 @@ databases.updateDocument(
"<COLLECTION_ID>", // collectionId
"<DOCUMENT_ID>", // documentId
mapOf( "a" to "b" ), // data (optional)
listOf("read("any")"), // permissions (optional)
listOf(Permission.read(Role.any())), // permissions (optional)
"<TRANSACTION_ID>", // transactionId (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -1,6 +1,8 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Databases;
import io.appwrite.Permission;
import io.appwrite.Role;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -13,7 +15,7 @@ databases.upsertDocument(
"<COLLECTION_ID>", // collectionId
"<DOCUMENT_ID>", // documentId
mapOf( "a" to "b" ), // data
listOf("read("any")"), // permissions (optional)
listOf(Permission.read(Role.any())), // permissions (optional)
"<TRANSACTION_ID>", // transactionId (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -11,6 +11,7 @@ Functions functions = new Functions(client);
functions.listExecutions(
"<FUNCTION_ID>", // functionId
listOf(), // queries (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -2,6 +2,8 @@ import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.models.InputFile;
import io.appwrite.services.Storage;
import io.appwrite.Permission;
import io.appwrite.Role;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -13,7 +15,7 @@ storage.createFile(
"<BUCKET_ID>", // bucketId
"<FILE_ID>", // fileId
InputFile.fromPath("file.png"), // file
listOf("read("any")"), // permissions (optional)
listOf(Permission.read(Role.any())), // permissions (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -12,6 +12,7 @@ storage.listFiles(
"<BUCKET_ID>", // bucketId
listOf(), // queries (optional)
"<SEARCH>", // search (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -1,6 +1,8 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Storage;
import io.appwrite.Permission;
import io.appwrite.Role;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -12,7 +14,7 @@ storage.updateFile(
"<BUCKET_ID>", // bucketId
"<FILE_ID>", // fileId
"<NAME>", // name (optional)
listOf("read("any")"), // permissions (optional)
listOf(Permission.read(Role.any())), // permissions (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -1,6 +1,8 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.TablesDB;
import io.appwrite.Permission;
import io.appwrite.Role;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -19,7 +21,7 @@ tablesDB.createRow(
"age" to 30,
"isAdmin" to false
), // data
listOf("read("any")"), // permissions (optional)
listOf(Permission.read(Role.any())), // permissions (optional)
"<TRANSACTION_ID>", // transactionId (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -13,6 +13,7 @@ tablesDB.listRows(
"<TABLE_ID>", // tableId
listOf(), // queries (optional)
"<TRANSACTION_ID>", // transactionId (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -1,6 +1,8 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.TablesDB;
import io.appwrite.Permission;
import io.appwrite.Role;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -13,7 +15,7 @@ tablesDB.updateRow(
"<TABLE_ID>", // tableId
"<ROW_ID>", // rowId
mapOf( "a" to "b" ), // data (optional)
listOf("read("any")"), // permissions (optional)
listOf(Permission.read(Role.any())), // permissions (optional)
"<TRANSACTION_ID>", // transactionId (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -1,6 +1,8 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.TablesDB;
import io.appwrite.Permission;
import io.appwrite.Role;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -13,7 +15,7 @@ tablesDB.upsertRow(
"<TABLE_ID>", // tableId
"<ROW_ID>", // rowId
mapOf( "a" to "b" ), // data (optional)
listOf("read("any")"), // permissions (optional)
listOf(Permission.read(Role.any())), // permissions (optional)
"<TRANSACTION_ID>", // transactionId (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -12,6 +12,7 @@ teams.listMemberships(
"<TEAM_ID>", // teamId
listOf(), // queries (optional)
"<SEARCH>", // search (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -11,6 +11,7 @@ Teams teams = new Teams(client);
teams.list(
listOf(), // queries (optional)
"<SEARCH>", // search (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -10,4 +10,5 @@ val account = Account(client)
val result = account.listIdentities(
queries = listOf(), // (optional)
total = false, // (optional)
)
@@ -10,4 +10,5 @@ val account = Account(client)
val result = account.listLogs(
queries = listOf(), // (optional)
total = false, // (optional)
)
@@ -0,0 +1,32 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.Avatars
val client = Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
.setProject("<YOUR_PROJECT_ID>") // Your project ID
val avatars = Avatars(client)
val result = avatars.getScreenshot(
url = "https://example.com",
headers = mapOf( "a" to "b" ), // (optional)
viewportWidth = 1, // (optional)
viewportHeight = 1, // (optional)
scale = 0.1, // (optional)
theme = theme.LIGHT, // (optional)
userAgent = "<USER_AGENT>", // (optional)
fullpage = false, // (optional)
locale = "<LOCALE>", // (optional)
timezone = timezone.AFRICA_ABIDJAN, // (optional)
latitude = -90, // (optional)
longitude = -180, // (optional)
accuracy = 0, // (optional)
touch = false, // (optional)
permissions = listOf(), // (optional)
sleep = 0, // (optional)
width = 0, // (optional)
height = 0, // (optional)
quality = -1, // (optional)
output = output.JPG, // (optional)
)
@@ -1,6 +1,8 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.Databases
import io.appwrite.Permission
import io.appwrite.Role
val client = Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -19,6 +21,6 @@ val result = databases.createDocument(
"age" to 30,
"isAdmin" to false
),
permissions = listOf("read("any")"), // (optional)
permissions = listOf(Permission.read(Role.any())), // (optional)
transactionId = "<TRANSACTION_ID>", // (optional)
)
@@ -13,4 +13,5 @@ val result = databases.listDocuments(
collectionId = "<COLLECTION_ID>",
queries = listOf(), // (optional)
transactionId = "<TRANSACTION_ID>", // (optional)
total = false, // (optional)
)
@@ -1,6 +1,8 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.Databases
import io.appwrite.Permission
import io.appwrite.Role
val client = Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -13,6 +15,6 @@ val result = databases.updateDocument(
collectionId = "<COLLECTION_ID>",
documentId = "<DOCUMENT_ID>",
data = mapOf( "a" to "b" ), // (optional)
permissions = listOf("read("any")"), // (optional)
permissions = listOf(Permission.read(Role.any())), // (optional)
transactionId = "<TRANSACTION_ID>", // (optional)
)
@@ -1,6 +1,8 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.Databases
import io.appwrite.Permission
import io.appwrite.Role
val client = Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -13,6 +15,6 @@ val result = databases.upsertDocument(
collectionId = "<COLLECTION_ID>",
documentId = "<DOCUMENT_ID>",
data = mapOf( "a" to "b" ),
permissions = listOf("read("any")"), // (optional)
permissions = listOf(Permission.read(Role.any())), // (optional)
transactionId = "<TRANSACTION_ID>", // (optional)
)
@@ -11,4 +11,5 @@ val functions = Functions(client)
val result = functions.listExecutions(
functionId = "<FUNCTION_ID>",
queries = listOf(), // (optional)
total = false, // (optional)
)
@@ -2,6 +2,8 @@ import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.models.InputFile
import io.appwrite.services.Storage
import io.appwrite.Permission
import io.appwrite.Role
val client = Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -13,5 +15,5 @@ val result = storage.createFile(
bucketId = "<BUCKET_ID>",
fileId = "<FILE_ID>",
file = InputFile.fromPath("file.png"),
permissions = listOf("read("any")"), // (optional)
permissions = listOf(Permission.read(Role.any())), // (optional)
)
@@ -12,4 +12,5 @@ val result = storage.listFiles(
bucketId = "<BUCKET_ID>",
queries = listOf(), // (optional)
search = "<SEARCH>", // (optional)
total = false, // (optional)
)
@@ -1,6 +1,8 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.Storage
import io.appwrite.Permission
import io.appwrite.Role
val client = Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -12,5 +14,5 @@ val result = storage.updateFile(
bucketId = "<BUCKET_ID>",
fileId = "<FILE_ID>",
name = "<NAME>", // (optional)
permissions = listOf("read("any")"), // (optional)
permissions = listOf(Permission.read(Role.any())), // (optional)
)
@@ -1,6 +1,8 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.TablesDB
import io.appwrite.Permission
import io.appwrite.Role
val client = Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -19,6 +21,6 @@ val result = tablesDB.createRow(
"age" to 30,
"isAdmin" to false
),
permissions = listOf("read("any")"), // (optional)
permissions = listOf(Permission.read(Role.any())), // (optional)
transactionId = "<TRANSACTION_ID>", // (optional)
)
@@ -13,4 +13,5 @@ val result = tablesDB.listRows(
tableId = "<TABLE_ID>",
queries = listOf(), // (optional)
transactionId = "<TRANSACTION_ID>", // (optional)
total = false, // (optional)
)
@@ -1,6 +1,8 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.TablesDB
import io.appwrite.Permission
import io.appwrite.Role
val client = Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -13,6 +15,6 @@ val result = tablesDB.updateRow(
tableId = "<TABLE_ID>",
rowId = "<ROW_ID>",
data = mapOf( "a" to "b" ), // (optional)
permissions = listOf("read("any")"), // (optional)
permissions = listOf(Permission.read(Role.any())), // (optional)
transactionId = "<TRANSACTION_ID>", // (optional)
)
@@ -1,6 +1,8 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.TablesDB
import io.appwrite.Permission
import io.appwrite.Role
val client = Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -13,6 +15,6 @@ val result = tablesDB.upsertRow(
tableId = "<TABLE_ID>",
rowId = "<ROW_ID>",
data = mapOf( "a" to "b" ), // (optional)
permissions = listOf("read("any")"), // (optional)
permissions = listOf(Permission.read(Role.any())), // (optional)
transactionId = "<TRANSACTION_ID>", // (optional)
)
@@ -12,4 +12,5 @@ val result = teams.listMemberships(
teamId = "<TEAM_ID>",
queries = listOf(), // (optional)
search = "<SEARCH>", // (optional)
total = false, // (optional)
)
@@ -11,4 +11,5 @@ val teams = Teams(client)
val result = teams.list(
queries = listOf(), // (optional)
search = "<SEARCH>", // (optional)
total = false, // (optional)
)
@@ -7,6 +7,7 @@ let client = Client()
let account = Account(client)
let identityList = try await account.listIdentities(
queries: [] // optional
queries: [], // optional
total: false // optional
)
@@ -7,6 +7,7 @@ let client = Client()
let account = Account(client)
let logList = try await account.listLogs(
queries: [] // optional
queries: [], // optional
total: false // optional
)
@@ -0,0 +1,32 @@
import Appwrite
import AppwriteEnums
let client = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
.setProject("<YOUR_PROJECT_ID>") // Your project ID
let avatars = Avatars(client)
let bytes = try await avatars.getScreenshot(
url: "https://example.com",
headers: [:], // optional
viewportWidth: 1, // optional
viewportHeight: 1, // optional
scale: 0.1, // optional
theme: .light, // optional
userAgent: "<USER_AGENT>", // optional
fullpage: false, // optional
locale: "<LOCALE>", // optional
timezone: .africaAbidjan, // optional
latitude: -90, // optional
longitude: -180, // optional
accuracy: 0, // optional
touch: false, // optional
permissions: [], // optional
sleep: 0, // optional
width: 0, // optional
height: 0, // optional
quality: -1, // optional
output: .jpg // optional
)

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