Compare commits

..
Author SHA1 Message Date
ArnabChatterjee20k 48bf79459f linting 2025-12-10 21:16:49 +05:30
ArnabChatterjee20k b4759adb03 updated identifier 2025-12-10 21:15:49 +05:30
ArnabChatterjee20k 6ce36e7a24 removed retry block for realtime startup collection check infite loop 2025-12-10 21:04:38 +05:30
ArnabChatterjee20k da1285bb22 updated time limit key, cached resource 2025-12-05 09:25:34 +05:30
ArnabChatterjee20k c4cc8595d6 removed redundant static realtime 2025-12-03 22:08:05 +05:30
ArnabChatterjee20k 5878a82b8a add user class 2025-12-03 21:54:45 +05:30
ArnabChatterjee20k 0353b826ac Refactor: use Coroutine context for database and cache functions 2025-12-03 21:32:22 +05:30
Jake BarnbyandGitHub 379b85bb7b Merge pull request #10891 from appwrite/fix-duplicate-document-error
Fix: Duplicate document error while creating file
2025-12-03 01:46:06 +00:00
Damodar LohaniandGitHub 97038c9d05 Merge branch '1.8.x' into fix-duplicate-document-error 2025-12-03 05:17:24 +05:45
Jake BarnbyandGitHub 5cc6845a44 Merge pull request #10888 from appwrite/fix-query-call
Fix invalid query
2025-12-02 06:48:28 +00:00
Damodar LohaniandGitHub 0386fde126 Merge pull request #10890 from appwrite/feat-update-geodb
Update GeoDB database
2025-12-02 11:48:20 +05:45
Damodar Lohani 866bd5ebc8 Fix: update test IP 2025-12-02 05:25:25 +00:00
Damodar Lohani d0b1f5f529 Fix: Catch duplicate exception while creating file document 2025-12-02 02:09:29 +00:00
Damodar Lohani 98dbb03155 Feat: update GeoDB database 2025-12-02 01:40:44 +00:00
Jake BarnbyandGitHub 088359c257 Merge pull request #10889 from appwrite/fix-error-setting-user-password
Fix: error setting user password
2025-12-02 01:23:48 +00:00
Damodar Lohani e114d49789 Fix: add test 2025-12-02 01:04:51 +00:00
Damodar Lohani 1df5b71e32 Simplify 2025-12-02 00:59:39 +00:00
Damodar Lohani a166ae5a03 Fix: error setting user password
Fixes Update Password Bug
Fixes #10878
2025-12-02 00:59:19 +00:00
Luke B. SilverandGitHub c5b2b67361 Merge pull request #10887 from appwrite/chore-bump-assistant-0.8.4
chore: bump assistant 0.8.4
2025-12-01 14:40:22 +00:00
Darshan 4c62ecbc95 fix: query call. 2025-12-01 19:59:07 +05:30
loks0n 1a4adc50b2 chore: bump assistant 0.8.4 2025-12-01 14:20:41 +00:00
Eldad A. FuxandGitHub adc0d3dbd0 Merge pull request #10884 from appwrite/fix-totp
Fix wrong path
2025-11-29 13:47:21 +01:00
Darshan 27b8b23c07 fix: wrong path. 2025-11-29 17:49:52 +05:30
Matej BačoandGitHub 64dbb28612 Merge pull request #10880 from appwrite/ser-504
Support query limit and offset in list repos API
2025-11-28 11:49:45 +01:00
Hemachandar 0b5d67a77a update vcs version 2025-11-28 15:43:31 +05:30
Hemachandar e204419b8b handle invalid offset 2025-11-28 15:33:32 +05:30
HemachandarandGitHub 851fb58645 Merge branch '1.8.x' into ser-504 2025-11-28 13:13:06 +05:30
Hemachandar 128478ed00 coderabbit feedback 2025-11-28 12:54:34 +05:30
Hemachandar 6f0c4d6c4e fix limit & offset computation 2025-11-28 12:41:04 +05:30
Matej BačoandGitHub c4366c9de1 Merge pull request #10877 from appwrite/ser-648
Fix file token expiry
2025-11-27 15:54:31 +01:00
Hemachandar d517ca8444 Merge branch '1.8.x' into ser-504 2025-11-27 20:12:58 +05:30
Hemachandar 67ae0b088a update composer.lock 2025-11-27 20:12:22 +05:30
Hemachandar adc5426ce9 feedback 2025-11-27 20:02:39 +05:30
Hemachandar c09f7d5cf9 Fix file token expiry 2025-11-27 19:40:09 +05:30
Damodar LohaniandGitHub 95abe61bab Merge pull request #10875 from appwrite/fix-wrong-user-type
Add teamId to project array in e2e test
2025-11-27 17:39:33 +05:45
Damodar Lohani 2fdfbf6e61 Fix type 2025-11-27 11:48:32 +00:00
Matej BačoandGitHub c2585e16b1 Merge pull request #10874 from appwrite/revert-10820-ser-550
Revert "Update deploymentId in sites rule for first deployment"
2025-11-27 12:28:55 +01:00
HemachandarandGitHub fd466dfcea Revert "Update deploymentId in sites rule for first deployment" 2025-11-27 16:31:14 +05:30
Damodar LohaniandGitHub 2bd308c236 Merge pull request #10758 from appwrite/feat-appwrite-auth
Feat: utopia auth
2025-11-27 15:11:20 +05:45
Jake BarnbyandGitHub 885a48b598 Merge pull request #10871 from appwrite/feat-custom-realtime-logger
Add realtime logger
2025-11-27 08:51:07 +00:00
Jake Barnby 63652cc155 Add realtime env on config 2025-11-27 17:28:10 +13:00
Damodar Lohani 06c876906c AI suggested fixes 2025-11-27 00:58:32 +00:00
Damodar Lohani ef8d7a45f9 composer update 2025-11-27 00:29:15 +00:00
Jake Barnby 04dd0f807d Add realtime logger 2025-11-27 00:25:04 +13:00
Damodar Lohani 5dd3f9c574 Format 2025-11-26 11:08:13 +00:00
Damodar Lohani cbc8b2ca0d Fix: validator not working 2025-11-26 11:02:00 +00:00
Damodar Lohani f6b04ae63e Fix type 2025-11-26 10:56:56 +00:00
Damodar Lohani 4fd0af509e composer update 2025-11-26 10:53:44 +00:00
Damodar Lohani d539186808 Merge remote-tracking branch 'origin/1.8.x' into feat-appwrite-auth 2025-11-26 10:42:55 +00:00
Hemachandar 1ed3cf0e59 Support query limit and offset in list repos API 2025-11-25 19:24:30 +05:30
Matej BačoandGitHub 11b8742a73 Merge pull request #10869 from appwrite/add-logs
Add logs for migrations sources
2025-11-25 13:09:22 +01:00
Darshan 7e9d74e141 bump: migrations for logs 2025-11-25 17:15:13 +05:30
Matej BačoandGitHub aa98fbb2dc Merge pull request #10855 from appwrite/update-sdk-examples-docs
Update SDK generator and regenerate Java/Kotlin documentation examples
2025-11-25 12:26:46 +01:00
Chirag Aggarwal 8f1b3a8e30 Merge branch '1.8.x' into update-sdk-examples-docs 2025-11-25 14:45:54 +05:30
Matej BačoandGitHub 3d8cee4676 Merge pull request #10820 from appwrite/ser-550
Update deploymentId in sites rule for first deployment
2025-11-25 09:50:16 +01:00
Matej BačoandGitHub 7efbae4310 Merge pull request #10793 from appwrite/feat-SER-448-improve-MFA-docs-endpoint-order
refactor(account): move MFA endpoints to module actions
2025-11-25 09:40:39 +01:00
Jake BarnbyandGitHub 1c9fd39fd6 Merge pull request #10850 from appwrite/browser-bump
Bump: browser version for tests.
2025-11-25 08:28:52 +00:00
Darshan 8c854b7d07 remove: screenshots uploader for comparison. 2025-11-25 13:26:33 +05:30
HemachandarandGitHub c713357131 Merge branch '1.8.x' into ser-550 2025-11-25 13:23:09 +05:30
Darshan fe9ecb6fcb simplify: index. 2025-11-25 13:22:19 +05:30
Darshan ab1541808b Merge remote-tracking branch 'origin/browser-bump' into browser-bump 2025-11-25 12:39:37 +05:30
Darshan 623a8a747d temp: compare the screenshots. 2025-11-25 12:39:31 +05:30
DarshanandGitHub 21237f8e02 Merge branch '1.8.x' into browser-bump 2025-11-25 12:22:51 +05:30
Harsh Mahajan f0ed75f585 update MFA challenge examples to use plural path 2025-11-25 06:08:10 +00:00
Harsh MahajanandGitHub 34ce5099e2 Merge branch '1.8.x' into feat-SER-448-improve-MFA-docs-endpoint-order 2025-11-25 11:30:25 +05:30
Chirag Aggarwal 10171c80da Update SDK generator and regenerate Java/Kotlin documentation examples
Updated SDK generator to fix Java examples Kotlin syntax issues and regenerated all affected documentation examples across multiple SDK versions.
2025-11-25 11:08:11 +05:30
Jake BarnbyandGitHub c6e4afdb49 Merge pull request #10867 from appwrite/feat-jwt-disposition
Custom disposition
2025-11-25 03:25:15 +00:00
Jake Barnby 448e60479d Downgrade migrations 2025-11-25 16:04:46 +13:00
Jake Barnby 8fe7de7ff9 Custom disposition 2025-11-25 15:19:16 +13:00
Matej BačoandGitHub 8e70a5239d Merge pull request #10860 from appwrite/fix-tanstack-nitro-default
Update TanStack root directory
2025-11-24 19:39:49 +01:00
Matej Bačo 28beb27759 Upgrade detection lib 2025-11-24 19:08:09 +01:00
Harsh MahajanGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
b1f5b6946a Update src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Create.php
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-11-24 17:44:25 +05:30
Harsh MahajanGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
9ac594c537 Update src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-11-24 17:44:03 +05:30
Matej Bačo 6547637c96 upgrade lib 2025-11-24 12:58:47 +01:00
Matej Bačo 13a7ead598 Fix TanStack Start default output directory 2025-11-24 12:08:55 +01:00
Harsh MahajanandGitHub d40e793137 Merge branch '1.8.x' into feat-SER-448-improve-MFA-docs-endpoint-order 2025-11-24 14:57:50 +05:30
Damodar Lohani 149fee503f Format 2025-11-23 07:01:11 +00:00
Damodar Lohani 5a1c257a37 composer update 2025-11-23 06:46:15 +00:00
Damodar Lohani 94ba8ee5f2 Merge remote-tracking branch 'origin/1.8.x' into feat-appwrite-auth 2025-11-23 06:43:52 +00:00
Darshan e5fa23ef6c add: 1:1 comparison tests. 2025-11-21 20:05:31 +05:30
DarshanandGitHub 053665eef9 Merge branch '1.8.x' into browser-bump 2025-11-21 13:51:27 +05:30
Jake BarnbyandGitHub 66afd1064d Merge pull request #10853 from appwrite/release-cli-12.0.0
chore: release cli 12.0.0
2025-11-21 08:16:30 +00:00
Harsh Mahajan e2758691af removed singular folder 2025-11-21 06:49:09 +00:00
DarshanandGitHub 9a8a42f479 Merge branch '1.8.x' into browser-bump 2025-11-21 12:10:21 +05:30
Darshan 3347573f17 bump: browser version for tests. 2025-11-21 12:09:33 +05:30
Harsh MahajanandGitHub cceb108fd3 Merge branch '1.8.x' into feat-SER-448-improve-MFA-docs-endpoint-order 2025-11-21 11:59:26 +05:30
Chirag Aggarwal b905686c0e fix typo 2025-11-21 10:28:02 +05:30
Chirag Aggarwal ad43be5dab fix version 2025-11-21 10:26:38 +05:30
Chirag Aggarwal ceb11e039f chore: release cli 12.0.0 2025-11-21 10:25:14 +05:30
Jake BarnbyandGitHub 63e06750cf Merge pull request #10851 from appwrite/chore-index-scope-description
chore: update index descriptions to reflect table terminology
2025-11-21 04:52:38 +00:00
Jake BarnbyandGitHub a4f7cf9f73 Merge pull request #10852 from appwrite/vcs-reference-type
chore: change VCSDeploymentType enum to VCSReferenceType
2025-11-21 04:51:46 +00:00
Chirag Aggarwal ba10bed271 chore: change VCSDeploymentType enum to VCSReferenceType 2025-11-21 09:56:26 +05:30
Steven NguyenandGitHub e2979410da chore: update index descriptions to reflect table terminology
Updated descriptions for indexes to refer to tables instead of collections.
2025-11-20 18:34:45 -08:00
Darshan 9b68b54743 bump: browser version for tests. 2025-11-20 20:18:54 +05:30
Matej BačoandGitHub 0fbdbb386f Merge pull request #10848 from appwrite/template-type-enum-override
fix: override template deployment type enum name
2025-11-20 11:54:40 +01:00
Chirag AggarwalandGitHub 50f26e8702 Merge branch '1.8.x' into template-type-enum-override 2025-11-20 16:07:07 +05:30
Chirag Aggarwal a2463f8cc3 fix naming 2025-11-20 15:23:16 +05:30
Jake BarnbyandGitHub 0cdabbf959 Merge pull request #10847 from appwrite/feat-csv-export
Set downloadUrl on realtime event
2025-11-20 06:01:06 +00:00
Chirag Aggarwal 4b752c15e8 functions too 2025-11-20 10:42:13 +05:30
Chirag Aggarwal fcce6f8d34 fix: override template deployment type enum name 2025-11-20 10:32:56 +05:30
Jake Barnby 2443266ac5 Set downloadUrl on realtime event 2025-11-20 17:10:47 +13:00
Matej BačoandGitHub 4616a2e95d Merge pull request #10838 from appwrite/release-flutter-20.3.2
chore: release flutter 20.3.2
2025-11-19 11:42:24 +01:00
Matej BačoandGitHub 9a0e1c1840 Merge pull request #10833 from appwrite/chore-config-for-env
Use config library for dotenv adapter
2025-11-19 11:40:09 +01:00
Matej Bačo bdbe94b56f Improce codeQL 2025-11-19 11:02:29 +01:00
Matej Bačo 0974e1335f Cleanup 2025-11-19 10:38:32 +01:00
Chirag Aggarwal 61f8c99da7 chore: release flutter 20.3.2 2025-11-19 14:11:40 +05:30
Matej Bačo 150d09f656 Update composer.lock 2025-11-19 09:27:53 +01:00
Matej Bačo bfce804f79 Merge branch '1.8.x' into chore-config-for-env 2025-11-19 09:27:42 +01:00
Jake BarnbyandGitHub a479ce63fc Merge pull request #10837 from appwrite/revert-refactor-auth-single-instance
Revert "Merge pull request #10682 from appwrite/refactor-auth-single-…
2025-11-19 04:16:57 +00:00
Jake Barnby 8765d5650a Revert "Merge pull request #10682 from appwrite/refactor-auth-single-instance"
This reverts commit 919b01a1b1, reversing
changes made to deb5039352.
2025-11-19 16:21:06 +13:00
Matej Bačo e3ee724178 Fix during QA 2025-11-18 16:42:57 +01:00
Matej Bačo ce2f2d0ebc Use config library for dotenv adapter 2025-11-18 16:35:02 +01:00
Hemachandar 9f7e3146a4 feedback 2025-11-18 20:08:43 +05:30
Matej BačoandGitHub e6039cf65e Merge pull request #10830 from appwrite/chore-formatinstance
chore: custom format instance in specs
2025-11-18 13:29:27 +01:00
Chirag AggarwalandGitHub 7b3bceeb7d Merge branch '1.8.x' into chore-formatinstance 2025-11-18 15:34:42 +05:30
Jake BarnbyandGitHub 90ef76ad82 Merge pull request #10828 from appwrite/fix-enum-examples
fix: a lot of enum examples generated
2025-11-18 08:10:39 +00:00
Chirag Aggarwal 5a8a900b18 format 2025-11-18 12:50:22 +05:30
Chirag Aggarwal c2645be4e1 chore: custom format instance in specs 2025-11-18 12:47:39 +05:30
Chirag Aggarwal 20221cd6b6 sync composer 2025-11-18 11:43:27 +05:30
Jake BarnbyandGitHub 275ff3dbf2 Merge pull request #10722 from appwrite/feat-per-bucket-image-transformations
feat: per bucket image transformations flag
2025-11-18 06:10:00 +00:00
Chirag Aggarwal b6fbddad81 fix: a lot of enum examples generated 2025-11-18 11:07:50 +05:30
Chirag Aggarwal 39c5b6c4d0 regenerate sdk examples 2025-11-18 09:53:55 +05:30
Chirag Aggarwal f9274dad3f regenerate specs 2025-11-18 09:53:13 +05:30
Chirag Aggarwal 3568af54b3 consistent naming 2025-11-18 09:51:20 +05:30
Chirag Aggarwal 71b6ac0364 Merge branch '1.8.x' into feat-per-bucket-image-transformations 2025-11-18 09:50:02 +05:30
Jake BarnbyandGitHub 919b01a1b1 Merge pull request #10682 from appwrite/refactor-auth-single-instance
Refactor authorization handling across multiple modules to use the ne…
2025-11-18 03:25:48 +00:00
Hemachandar 3d69998f9c another test fix 2025-11-17 18:41:28 +05:30
Hemachandar d2000ee9d8 test fix 2025-11-17 17:58:15 +05:30
Hemachandar 0718f90405 test failures due to merge 2025-11-17 15:51:36 +05:30
Hemachandar 73f5b4227d nit 2025-11-17 14:58:04 +05:30
Hemachandar 52766cfdba Merge branch '1.8.x' into ser-550 2025-11-17 14:57:00 +05:30
Hemachandar ba542554ce functions and more tests 2025-11-17 14:53:26 +05:30
Matej BačoandGitHub deb5039352 Merge pull request #10795 from appwrite/feat-suggested-env-vars
Feat:  Git env variables suggestion
2025-11-14 20:43:26 +01:00
Matej Bačo 793057c823 Update composer.lock 2025-11-14 18:06:20 +01:00
Matej Bačo 4a86b2d5b1 Merge branch '1.8.x' into feat-suggested-env-vars 2025-11-14 18:05:21 +01:00
Hemachandar d36e42377b include VCS 2025-11-14 20:49:06 +05:30
Hemachandar d9522ac6b8 Update deploymentId in sites rule for first deployment 2025-11-14 20:44:48 +05:30
Matej Bačo db1b87ecbb Add env var value detection 2025-11-14 15:49:19 +01:00
Luke B. SilverandGitHub 99d16dfc60 Merge pull request #10819 from appwrite/fix-missing-nullable
fix: missing nullable in datetime
2025-11-14 10:00:06 +00:00
Luke B. SilverandGitHub 725870b1b1 Merge pull request #10817 from appwrite/update-release-sdks
fix: release sdks doc
2025-11-14 09:40:40 +00:00
Chirag Aggarwal 38ef0ad399 fix: release sdks doc 2025-11-14 12:39:37 +05:30
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 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
shimon 1da4736d5d composer.lock 2025-11-13 11:23:26 +02: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
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
Chirag AggarwalandGitHub 653ac184e1 Merge branch '1.8.x' into feat-per-bucket-image-transformations 2025-11-11 13:53:29 +05:30
Harsh Mahajan b20c493fa6 changes 2025-11-11 07:50:42 +00:00
Harsh MahajanandGitHub b91268b3cd Merge branch '1.8.x' into feat-SER-448-improve-MFA-docs-endpoint-order 2025-11-11 00:13:52 +05:30
Matej Bačo 831a261c04 Sync unit tests with DNS lib validator 2025-11-10 16:29:34 +01:00
Matej Bačo c784cf950b Add variables suggestion 2025-11-10 16:20:48 +01:00
Harsh Mahajan 64b4ea9551 refactor(account): move MFA endpoints to module actions 2025-11-10 11:56:08 +00:00
Damodar Lohani 47b653fd64 Use correct hashing algorithm for oauth2 token 2025-11-10 11:38:47 +00:00
Damodar Lohani c8e6fcc090 fix token verify 2025-11-10 09:01:12 +00:00
Damodar Lohani b3df65cf33 remove unused var 2025-11-10 09:01:09 +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 LohaniandGitHub 62e5f54fcb Merge branch '1.8.x' into feat-appwrite-auth 2025-11-09 08:36:27 +05:45
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
Damodar Lohani 9ef6fa9b4c Merge remote-tracking branch 'origin/1.8.x' into feat-appwrite-auth 2025-11-06 05:58:51 +00:00
Damodar Lohani 9fe74bdd22 update database library 2025-11-06 05:57:36 +00:00
Damodar Lohani fa2b46c7e7 Fix: dev dependency 2025-11-06 02:26:47 +00:00
Damodar Lohani 13f9900802 fix version 2025-11-06 02:12:00 +00:00
Damodar Lohani 3c93004cde Fix: switch database version 2025-11-06 02:05:18 +00:00
Damodar Lohani 631fd2ea4c Merge remote-tracking branch 'origin/1.8.x' into feat-appwrite-auth 2025-11-06 02:04:38 +00: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
Damodar Lohani eb7be569eb format 2025-11-05 07:10:47 +00:00
Damodar Lohani 88cbc36a02 remove Appwrite\Auth\Auth 2025-11-05 07:09:18 +00:00
Damodar Lohani 6a60b7d25b format 2025-11-05 05:33:22 +00:00
Damodar Lohani 343d443903 Fix realtime 2025-11-05 05:31:17 +00:00
Damodar Lohani 12c27edaf9 Fix user test 2025-11-05 05:23:32 +00:00
Damodar Lohani 0f9b13e251 use argon default 2025-11-05 05:09:36 +00:00
Damodar Lohani 965c4738e7 fix user's attributes 2025-11-05 05:06:40 +00:00
Damodar Lohani 4679eff4b8 use argon 2 default 2025-11-05 03:34:52 +00:00
Damodar Lohani 3cfb5d695f Fix list identities 2025-11-05 03:33:02 +00:00
Jake BarnbyandGitHub 501cba216b Update app/init/resources.php 2025-11-05 16:09:20 +13:00
Damodar Lohani 7ef5a7edaf Fix teams 2025-11-05 02:39:03 +00:00
Damodar Lohani 8adeb40c1d fix team get membership 2025-11-05 02:33:22 +00:00
Damodar Lohani ae105165ab fix team logs endpoint 2025-11-05 02:28:56 +00:00
Damodar Lohani 5dfee10369 Fix logs endpoint 2025-11-05 02:16:37 +00:00
Damodar Lohani 44b5ed90d5 composer update 2025-11-05 02:06:14 +00:00
Damodar Lohani e70d5c46b6 Merge branch 'feat-appwrite-auth' of https://github.com/appwrite/appwrite into feat-appwrite-auth 2025-11-05 02:05:25 +00:00
Damodar Lohani c9d66c52b4 composer update 2025-11-05 02:04:39 +00:00
Damodar Lohani 4d22f4252f Merge remote-tracking branch 'origin/1.8.x' into feat-appwrite-auth 2025-11-05 01:59:42 +00:00
Damodar Lohani 9130a7102e Fix use correct user type 2025-11-05 01:41:15 +00:00
Damodar Lohani 2a7d89ea7b Fix missing auth vars 2025-11-05 01:14:36 +00: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
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
Chirag AggarwalandGitHub decad9ff9d Merge branch '1.8.x' into feat-per-bucket-image-transformations 2025-11-04 14:31:04 +05:30
Damodar LohaniandGitHub e264ef53f4 Merge branch '1.8.x' into feat-appwrite-auth 2025-11-04 14:08:15 +05:45
Damodar Lohani 31ffc9ea92 format 2025-11-04 08:12:46 +00:00
Damodar Lohani 81480f4492 remove deprecated methods 2025-11-04 07:53:14 +00:00
Damodar Lohani 4b15fec1d7 Remove further auth static methods 2025-11-04 07:51:03 +00:00
Damodar Lohani 918ad4a545 remove auth test and add user test 2025-11-04 07:41:42 +00:00
Damodar Lohani dff7aa6ce0 format 2025-11-04 07:12:42 +00:00
Damodar Lohani 5ef717d0fd More replacement for auth checks 2025-11-04 07:12:34 +00:00
Damodar Lohani 437d914e90 More refactor and fixes 2025-11-04 06:08:35 +00:00
Damodar Lohani 897f512b47 Refactor remove moved to utopia auth 2025-11-04 05:37:45 +00:00
Damodar Lohani df1f14c853 Refactor realtime 2025-11-04 05:25:20 +00:00
Damodar Lohani 68cce71527 refactor realtime 2025-11-04 05:22:18 +00:00
Damodar Lohani 2ae474971d use new session verify 2025-11-04 03:48:57 +00:00
Damodar Lohani 617c1aaee3 update db with custom types support 2025-11-04 02:34:07 +00:00
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
Damodar Lohani 50cd6a737d Update resources 2025-11-03 09:39:52 +00:00
Damodar Lohani a575c07468 Fix user resource 2025-11-03 09:32:56 +00:00
Damodar Lohani 071ec671e0 Feat: utopia auth 2025-11-03 09:13:08 +00: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
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
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
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 a6d1f46133 addressing comments 2025-10-29 10:38:09 +02: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
Chirag AggarwalandGitHub 02e8a7b429 Merge branch '1.8.x' into feat-per-bucket-image-transformations 2025-10-28 15:12:35 +05:30
Chirag Aggarwal ca861119f9 keep in same 2025-10-28 15:10:51 +05:30
Chirag Aggarwal 0bfbae4cdd add to both dbs 2025-10-28 15:09:11 +05:30
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
Chirag Aggarwal 7b1e163c77 move to check 2025-10-28 15:07:24 +05:30
Chirag Aggarwal 3f710ddc70 fix: test 2025-10-28 15:06:43 +05:30
Chirag Aggarwal 1546add5d5 add to both dbs 2025-10-28 15:05:14 +05:30
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
Chirag Aggarwal 4aaaa460b2 feat: per bucket image transformations flag 2025-10-28 14:13:38 +05:30
Shimon NewmanandGitHub dee3d375db Merge branch '1.8.x' into refactor-auth-single-instance 2025-10-28 10:01:33 +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
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
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
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
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
495 changed files with 5397 additions and 4303 deletions
+1
View File
@@ -103,6 +103,7 @@ _APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000
_APP_MAINTENANCE_RETENTION_SCHEDULES=86400
_APP_USAGE_STATS=enabled
_APP_LOGGING_CONFIG=
_APP_LOGGING_CONFIG_REALTIME=
_APP_GRAPHQL_MAX_BATCH_SIZE=10
_APP_GRAPHQL_MAX_COMPLEXITY=250
_APP_GRAPHQL_MAX_DEPTH=4
+2 -2
View File
@@ -24,9 +24,9 @@ ENV _APP_VERSION=$VERSION \
_APP_HOME=https://appwrite.io
RUN \
if [ "$DEBUG" == "true" ]; then \
if [ "$DEBUG" == "true" ]; then \
apk add boost boost-dev; \
fi
fi
WORKDIR /usr/src/code
Binary file not shown.
Binary file not shown.
+2
View File
@@ -9,6 +9,7 @@ use Appwrite\Event\StatsResources;
use Appwrite\Event\StatsUsage;
use Appwrite\Platform\Appwrite;
use Appwrite\Runtimes\Runtimes;
use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Swoole\Runtime;
use Swoole\Timer;
@@ -76,6 +77,7 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) {
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', 'console');
$dbForPlatform->setDocumentType('users', User::class);
// Ensure tables exist
$collections = Config::getParam('collections', [])['console'];
+14 -3
View File
@@ -1,6 +1,6 @@
<?php
use Appwrite\Auth\Auth;
use Utopia\Auth\Hashes\Argon2;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
@@ -173,7 +173,7 @@ return [
'size' => 256,
'signed' => true,
'required' => false,
'default' => Auth::DEFAULT_ALGO,
'default' => (new Argon2())->getName(),
'array' => false,
'filters' => [],
],
@@ -184,7 +184,7 @@ return [
'size' => 65535,
'signed' => true,
'required' => false,
'default' => Auth::DEFAULT_ALGO_OPTIONS,
'default' => (new Argon2())->getOptions(),
'array' => false,
'filters' => ['json'],
],
@@ -1582,6 +1582,17 @@ return [
'required' => true,
'array' => false,
],
[
'$id' => ID::custom('transformations'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => false,
'array' => false,
'default' => true,
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
+1 -2
View File
@@ -4,7 +4,6 @@
* Initializes console project document.
*/
use Appwrite\Auth\Auth;
use Appwrite\Network\Platform;
use Utopia\Database\Helpers\ID;
use Utopia\System\System;
@@ -38,7 +37,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' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
'duration' => TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled',
'invalidateSessions' => true
],
+5
View File
@@ -522,6 +522,11 @@ return [
'description' => 'The requested file is not publicly readable.',
'code' => 403,
],
Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED => [
'name' => Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED,
'description' => 'Image transformations are disabled for the requested bucket.',
'code' => 403,
],
/** Tokens */
Exception::TOKEN_NOT_FOUND => [
+1 -1
View File
@@ -215,7 +215,7 @@ return [
'key' => 'ssr',
'buildCommand' => 'npm run build',
'installCommand' => 'npm install',
'outputDirectory' => './dist',
'outputDirectory' => './.output',
'startCommand' => 'bash helpers/tanstack-start/server.sh',
],
'static' => [
+2 -2
View File
@@ -60,7 +60,7 @@ return [
[
'key' => 'flutter',
'name' => 'Flutter',
'version' => '20.3.1',
'version' => '20.3.2',
'url' => 'https://github.com/appwrite/sdk-for-flutter',
'package' => 'https://pub.dev/packages/appwrite',
'enabled' => true,
@@ -226,7 +226,7 @@ return [
[
'key' => 'cli',
'name' => 'Command Line',
'version' => '11.1.1',
'version' => '12.0.0',
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
+7 -7
View File
@@ -1,6 +1,6 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Utopia\Database\Documents\User;
$member = [
'global',
@@ -92,7 +92,7 @@ $admins = [
];
return [
Auth::USER_ROLE_GUESTS => [
User::ROLE_GUESTS => [
'label' => 'Guests',
'scopes' => [
'global',
@@ -112,23 +112,23 @@ return [
'execution.write',
],
],
Auth::USER_ROLE_USERS => [
User::ROLE_USERS => [
'label' => 'Users',
'scopes' => \array_merge($member),
],
Auth::USER_ROLE_ADMIN => [
User::ROLE_ADMIN => [
'label' => 'Admin',
'scopes' => \array_merge($admins),
],
Auth::USER_ROLE_DEVELOPER => [
User::ROLE_DEVELOPER => [
'label' => 'Developer',
'scopes' => \array_merge($admins),
],
Auth::USER_ROLE_OWNER => [
User::ROLE_OWNER => [
'label' => 'Owner',
'scopes' => \array_merge($member, $admins),
],
Auth::USER_ROLE_APPS => [
User::ROLE_APPS => [
'label' => 'Applications',
'scopes' => ['global', 'health.read', 'graphql'],
],
+2 -2
View File
@@ -47,10 +47,10 @@ return [ // List of publicly visible scopes
'description' => 'Access to create, update, and delete your project\'s database table\'s columns',
],
'indexes.read' => [
'description' => 'Access to read your project\'s database collection\'s indexes',
'description' => 'Access to read your project\'s database table\'s indexes',
],
'indexes.write' => [
'description' => 'Access to create, update, and delete your project\'s database collection\'s indexes',
'description' => 'Access to create, update, and delete your project\'s database table\'s indexes',
],
'documents.read' => [
'description' => 'Access to read your project\'s database documents',
+155 -18
View File
@@ -7753,7 +7753,8 @@
"default": {
"type": "string",
"description": "Default value for the attribute in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. Cannot be set when attribute is required.",
"x-example": null
"x-example": null,
"x-nullable": true
},
"array": {
"type": "boolean",
@@ -14414,10 +14415,22 @@
"description": "Path to function code in the template repo.",
"x-example": "<ROOT_DIRECTORY>"
},
"version": {
"type": {
"type": "string",
"description": "Version (tag) for the repo linked to the function template.",
"x-example": "<VERSION>"
"description": "Type for the reference provided. Can be commit, branch, or tag",
"x-example": "commit",
"enum": [
"commit",
"branch",
"tag"
],
"x-enum-name": "TemplateReferenceType",
"x-enum-keys": []
},
"reference": {
"type": "string",
"description": "Reference value, can be a commit hash, branch name, or release tag",
"x-example": "<REFERENCE>"
},
"activate": {
"type": "boolean",
@@ -14429,7 +14442,8 @@
"repository",
"owner",
"rootDirectory",
"version"
"type",
"reference"
]
}
}
@@ -14510,7 +14524,7 @@
"branch",
"commit"
],
"x-enum-name": "VCSDeploymentType",
"x-enum-name": "VCSReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -32314,10 +32328,22 @@
"description": "Path to site code in the template repo.",
"x-example": "<ROOT_DIRECTORY>"
},
"version": {
"type": {
"type": "string",
"description": "Version (tag) for the repo linked to the site template.",
"x-example": "<VERSION>"
"description": "Type for the reference provided. Can be commit, branch, or tag",
"x-example": "branch",
"enum": [
"branch",
"commit",
"tag"
],
"x-enum-name": "TemplateReferenceType",
"x-enum-keys": []
},
"reference": {
"type": "string",
"description": "Reference value, can be a commit hash, branch name, or release tag",
"x-example": "<REFERENCE>"
},
"activate": {
"type": "boolean",
@@ -32329,7 +32355,8 @@
"repository",
"owner",
"rootDirectory",
"version"
"type",
"reference"
]
}
}
@@ -32411,7 +32438,7 @@
"commit",
"tag"
],
"x-enum-name": "VCSDeploymentType",
"x-enum-name": "VCSReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -33455,7 +33482,7 @@
"parameters": [
{
"name": "queries",
"description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: enabled, name, fileSecurity, maximumFileSize, encryption, antivirus",
"description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: enabled, name, fileSecurity, maximumFileSize, encryption, antivirus, transformations",
"required": false,
"schema": {
"type": "array",
@@ -33605,6 +33632,11 @@
"type": "boolean",
"description": "Is virus scanning enabled? For file size above 20MB AntiVirus scanning is skipped even if it's enabled",
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Are image transformations enabled?",
"x-example": false
}
},
"required": [
@@ -33799,6 +33831,11 @@
"type": "boolean",
"description": "Is virus scanning enabled? For file size above 20MB AntiVirus scanning is skipped even if it's enabled",
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Are image transformations enabled?",
"x-example": false
}
},
"required": [
@@ -36096,7 +36133,7 @@
},
"rowSecurity": {
"type": "boolean",
"description": "Enables configuring permissions for individual rows. A user needs one of row or table level permissions to access a document. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"description": "Enables configuring permissions for individual rows. A user needs one of row or table-level permissions to access a row. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"x-example": false
},
"enabled": {
@@ -36593,7 +36630,8 @@
"default": {
"type": "string",
"description": "Default value for the column in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. Cannot be set when column is required.",
"x-example": null
"x-example": null,
"x-nullable": true
},
"array": {
"type": "boolean",
@@ -53579,6 +53617,11 @@
"type": "boolean",
"description": "Virus scanning is enabled.",
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Image transformations are enabled.",
"x-example": false
}
},
"required": [
@@ -53593,7 +53636,8 @@
"allowedFileExtensions",
"compression",
"encryption",
"antivirus"
"antivirus",
"transformations"
],
"example": {
"$id": "5e5ea5c16897e",
@@ -53612,7 +53656,8 @@
],
"compression": "gzip",
"encryption": false,
"antivirus": false
"antivirus": false,
"transformations": false
}
},
"resourceToken": {
@@ -54798,6 +54843,17 @@
"type": "string",
"description": "Last commit date in ISO 8601 format.",
"x-example": "datetime"
},
"variables": {
"type": "array",
"description": "Environment variables found in .env files",
"items": {
"type": "string"
},
"x-example": [
"PORT",
"NODE_ENV"
]
}
},
"required": [
@@ -54807,7 +54863,8 @@
"provider",
"private",
"defaultBranch",
"pushedAt"
"pushedAt",
"variables"
],
"example": {
"id": "5e5ea5c16897e",
@@ -54816,7 +54873,11 @@
"provider": "github",
"private": true,
"defaultBranch": "main",
"pushedAt": "datetime"
"pushedAt": "datetime",
"variables": [
"PORT",
"NODE_ENV"
]
}
},
"providerRepositoryFramework": {
@@ -54858,6 +54919,17 @@
"description": "Last commit date in ISO 8601 format.",
"x-example": "datetime"
},
"variables": {
"type": "array",
"description": "Environment variables found in .env files",
"items": {
"type": "string"
},
"x-example": [
"PORT",
"NODE_ENV"
]
},
"framework": {
"type": "string",
"description": "Auto-detected framework. Empty if type is not \"framework\".",
@@ -54872,6 +54944,7 @@
"private",
"defaultBranch",
"pushedAt",
"variables",
"framework"
],
"example": {
@@ -54882,6 +54955,10 @@
"private": true,
"defaultBranch": "main",
"pushedAt": "datetime",
"variables": [
"PORT",
"NODE_ENV"
],
"framework": "nextjs"
}
},
@@ -54924,6 +55001,17 @@
"description": "Last commit date in ISO 8601 format.",
"x-example": "datetime"
},
"variables": {
"type": "array",
"description": "Environment variables found in .env files",
"items": {
"type": "string"
},
"x-example": [
"PORT",
"NODE_ENV"
]
},
"runtime": {
"type": "string",
"description": "Auto-detected runtime. Empty if type is not \"runtime\".",
@@ -54938,6 +55026,7 @@
"private",
"defaultBranch",
"pushedAt",
"variables",
"runtime"
],
"example": {
@@ -54948,6 +55037,10 @@
"private": true,
"defaultBranch": "main",
"pushedAt": "datetime",
"variables": [
"PORT",
"NODE_ENV"
],
"runtime": "node-22"
}
},
@@ -54955,6 +55048,15 @@
"description": "DetectionFramework",
"type": "object",
"properties": {
"variables": {
"type": "array",
"description": "Environment variables found in .env files",
"items": {
"$ref": "#\/components\/schemas\/detectionVariable"
},
"x-example": {},
"nullable": true
},
"framework": {
"type": "string",
"description": "Framework",
@@ -54983,6 +55085,7 @@
"outputDirectory"
],
"example": {
"variables": {},
"framework": "nuxt",
"installCommand": "npm install",
"buildCommand": "npm run build",
@@ -54993,6 +55096,15 @@
"description": "DetectionRuntime",
"type": "object",
"properties": {
"variables": {
"type": "array",
"description": "Environment variables found in .env files",
"items": {
"$ref": "#\/components\/schemas\/detectionVariable"
},
"x-example": {},
"nullable": true
},
"runtime": {
"type": "string",
"description": "Runtime",
@@ -55015,11 +55127,36 @@
"commands"
],
"example": {
"variables": {},
"runtime": "node",
"entrypoint": "index.js",
"commands": "npm install && npm run build"
}
},
"detectionVariable": {
"description": "DetectionVariable",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of environment variable",
"x-example": "NODE_ENV"
},
"value": {
"type": "string",
"description": "Value of environment variable",
"x-example": "production"
}
},
"required": [
"name",
"value"
],
"example": {
"name": "NODE_ENV",
"value": "production"
}
},
"vcsContent": {
"description": "VcsContents",
"type": "object",
+61 -16
View File
@@ -7222,7 +7222,8 @@
"default": {
"type": "string",
"description": "Default value for the attribute in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. Cannot be set when attribute is required.",
"x-example": null
"x-example": null,
"x-nullable": true
},
"array": {
"type": "boolean",
@@ -13203,10 +13204,22 @@
"description": "Path to function code in the template repo.",
"x-example": "<ROOT_DIRECTORY>"
},
"version": {
"type": {
"type": "string",
"description": "Version (tag) for the repo linked to the function template.",
"x-example": "<VERSION>"
"description": "Type for the reference provided. Can be commit, branch, or tag",
"x-example": "commit",
"enum": [
"commit",
"branch",
"tag"
],
"x-enum-name": "TemplateReferenceType",
"x-enum-keys": []
},
"reference": {
"type": "string",
"description": "Reference value, can be a commit hash, branch name, or release tag",
"x-example": "<REFERENCE>"
},
"activate": {
"type": "boolean",
@@ -13218,7 +13231,8 @@
"repository",
"owner",
"rootDirectory",
"version"
"type",
"reference"
]
}
}
@@ -13300,7 +13314,7 @@
"branch",
"commit"
],
"x-enum-name": "VCSDeploymentType",
"x-enum-name": "VCSReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -22838,10 +22852,22 @@
"description": "Path to site code in the template repo.",
"x-example": "<ROOT_DIRECTORY>"
},
"version": {
"type": {
"type": "string",
"description": "Version (tag) for the repo linked to the site template.",
"x-example": "<VERSION>"
"description": "Type for the reference provided. Can be commit, branch, or tag",
"x-example": "branch",
"enum": [
"branch",
"commit",
"tag"
],
"x-enum-name": "TemplateReferenceType",
"x-enum-keys": []
},
"reference": {
"type": "string",
"description": "Reference value, can be a commit hash, branch name, or release tag",
"x-example": "<REFERENCE>"
},
"activate": {
"type": "boolean",
@@ -22853,7 +22879,8 @@
"repository",
"owner",
"rootDirectory",
"version"
"type",
"reference"
]
}
}
@@ -22936,7 +22963,7 @@
"commit",
"tag"
],
"x-enum-name": "VCSDeploymentType",
"x-enum-name": "VCSReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -23911,7 +23938,7 @@
"parameters": [
{
"name": "queries",
"description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: enabled, name, fileSecurity, maximumFileSize, encryption, antivirus",
"description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: enabled, name, fileSecurity, maximumFileSize, encryption, antivirus, transformations",
"required": false,
"schema": {
"type": "array",
@@ -24062,6 +24089,11 @@
"type": "boolean",
"description": "Is virus scanning enabled? For file size above 20MB AntiVirus scanning is skipped even if it's enabled",
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Are image transformations enabled?",
"x-example": false
}
},
"required": [
@@ -24258,6 +24290,11 @@
"type": "boolean",
"description": "Is virus scanning enabled? For file size above 20MB AntiVirus scanning is skipped even if it's enabled",
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Are image transformations enabled?",
"x-example": false
}
},
"required": [
@@ -26342,7 +26379,7 @@
},
"rowSecurity": {
"type": "boolean",
"description": "Enables configuring permissions for individual rows. A user needs one of row or table level permissions to access a document. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"description": "Enables configuring permissions for individual rows. A user needs one of row or table-level permissions to access a row. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"x-example": false
},
"enabled": {
@@ -26844,7 +26881,8 @@
"default": {
"type": "string",
"description": "Default value for the column in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. Cannot be set when column is required.",
"x-example": null
"x-example": null,
"x-nullable": true
},
"array": {
"type": "boolean",
@@ -42256,6 +42294,11 @@
"type": "boolean",
"description": "Virus scanning is enabled.",
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Image transformations are enabled.",
"x-example": false
}
},
"required": [
@@ -42270,7 +42313,8 @@
"allowedFileExtensions",
"compression",
"encryption",
"antivirus"
"antivirus",
"transformations"
],
"example": {
"$id": "5e5ea5c16897e",
@@ -42289,7 +42333,8 @@
],
"compression": "gzip",
"encryption": false,
"antivirus": false
"antivirus": false,
"transformations": false
}
},
"resourceToken": {
+123 -12
View File
@@ -7753,7 +7753,8 @@
"default": {
"type": "string",
"description": "Default value for the attribute in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. Cannot be set when attribute is required.",
"x-example": null
"x-example": null,
"x-nullable": true
},
"array": {
"type": "boolean",
@@ -14423,7 +14424,7 @@
"branch",
"tag"
],
"x-enum-name": null,
"x-enum-name": "TemplateReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -14523,7 +14524,7 @@
"branch",
"commit"
],
"x-enum-name": "VCSDeploymentType",
"x-enum-name": "VCSReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -32336,7 +32337,7 @@
"commit",
"tag"
],
"x-enum-name": null,
"x-enum-name": "TemplateReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -32437,7 +32438,7 @@
"commit",
"tag"
],
"x-enum-name": "VCSDeploymentType",
"x-enum-name": "VCSReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -33481,7 +33482,7 @@
"parameters": [
{
"name": "queries",
"description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: enabled, name, fileSecurity, maximumFileSize, encryption, antivirus",
"description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: enabled, name, fileSecurity, maximumFileSize, encryption, antivirus, transformations",
"required": false,
"schema": {
"type": "array",
@@ -33631,6 +33632,11 @@
"type": "boolean",
"description": "Is virus scanning enabled? For file size above 20MB AntiVirus scanning is skipped even if it's enabled",
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Are image transformations enabled?",
"x-example": false
}
},
"required": [
@@ -33825,6 +33831,11 @@
"type": "boolean",
"description": "Is virus scanning enabled? For file size above 20MB AntiVirus scanning is skipped even if it's enabled",
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Are image transformations enabled?",
"x-example": false
}
},
"required": [
@@ -36122,7 +36133,7 @@
},
"rowSecurity": {
"type": "boolean",
"description": "Enables configuring permissions for individual rows. A user needs one of row or table level permissions to access a document. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"description": "Enables configuring permissions for individual rows. A user needs one of row or table-level permissions to access a row. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"x-example": false
},
"enabled": {
@@ -36619,7 +36630,8 @@
"default": {
"type": "string",
"description": "Default value for the column in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. Cannot be set when column is required.",
"x-example": null
"x-example": null,
"x-nullable": true
},
"array": {
"type": "boolean",
@@ -53605,6 +53617,11 @@
"type": "boolean",
"description": "Virus scanning is enabled.",
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Image transformations are enabled.",
"x-example": false
}
},
"required": [
@@ -53619,7 +53636,8 @@
"allowedFileExtensions",
"compression",
"encryption",
"antivirus"
"antivirus",
"transformations"
],
"example": {
"$id": "5e5ea5c16897e",
@@ -53638,7 +53656,8 @@
],
"compression": "gzip",
"encryption": false,
"antivirus": false
"antivirus": false,
"transformations": false
}
},
"resourceToken": {
@@ -54824,6 +54843,17 @@
"type": "string",
"description": "Last commit date in ISO 8601 format.",
"x-example": "datetime"
},
"variables": {
"type": "array",
"description": "Environment variables found in .env files",
"items": {
"type": "string"
},
"x-example": [
"PORT",
"NODE_ENV"
]
}
},
"required": [
@@ -54833,7 +54863,8 @@
"provider",
"private",
"defaultBranch",
"pushedAt"
"pushedAt",
"variables"
],
"example": {
"id": "5e5ea5c16897e",
@@ -54842,7 +54873,11 @@
"provider": "github",
"private": true,
"defaultBranch": "main",
"pushedAt": "datetime"
"pushedAt": "datetime",
"variables": [
"PORT",
"NODE_ENV"
]
}
},
"providerRepositoryFramework": {
@@ -54884,6 +54919,17 @@
"description": "Last commit date in ISO 8601 format.",
"x-example": "datetime"
},
"variables": {
"type": "array",
"description": "Environment variables found in .env files",
"items": {
"type": "string"
},
"x-example": [
"PORT",
"NODE_ENV"
]
},
"framework": {
"type": "string",
"description": "Auto-detected framework. Empty if type is not \"framework\".",
@@ -54898,6 +54944,7 @@
"private",
"defaultBranch",
"pushedAt",
"variables",
"framework"
],
"example": {
@@ -54908,6 +54955,10 @@
"private": true,
"defaultBranch": "main",
"pushedAt": "datetime",
"variables": [
"PORT",
"NODE_ENV"
],
"framework": "nextjs"
}
},
@@ -54950,6 +55001,17 @@
"description": "Last commit date in ISO 8601 format.",
"x-example": "datetime"
},
"variables": {
"type": "array",
"description": "Environment variables found in .env files",
"items": {
"type": "string"
},
"x-example": [
"PORT",
"NODE_ENV"
]
},
"runtime": {
"type": "string",
"description": "Auto-detected runtime. Empty if type is not \"runtime\".",
@@ -54964,6 +55026,7 @@
"private",
"defaultBranch",
"pushedAt",
"variables",
"runtime"
],
"example": {
@@ -54974,6 +55037,10 @@
"private": true,
"defaultBranch": "main",
"pushedAt": "datetime",
"variables": [
"PORT",
"NODE_ENV"
],
"runtime": "node-22"
}
},
@@ -54981,6 +55048,15 @@
"description": "DetectionFramework",
"type": "object",
"properties": {
"variables": {
"type": "array",
"description": "Environment variables found in .env files",
"items": {
"$ref": "#\/components\/schemas\/detectionVariable"
},
"x-example": {},
"nullable": true
},
"framework": {
"type": "string",
"description": "Framework",
@@ -55009,6 +55085,7 @@
"outputDirectory"
],
"example": {
"variables": {},
"framework": "nuxt",
"installCommand": "npm install",
"buildCommand": "npm run build",
@@ -55019,6 +55096,15 @@
"description": "DetectionRuntime",
"type": "object",
"properties": {
"variables": {
"type": "array",
"description": "Environment variables found in .env files",
"items": {
"$ref": "#\/components\/schemas\/detectionVariable"
},
"x-example": {},
"nullable": true
},
"runtime": {
"type": "string",
"description": "Runtime",
@@ -55041,11 +55127,36 @@
"commands"
],
"example": {
"variables": {},
"runtime": "node",
"entrypoint": "index.js",
"commands": "npm install && npm run build"
}
},
"detectionVariable": {
"description": "DetectionVariable",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of environment variable",
"x-example": "NODE_ENV"
},
"value": {
"type": "string",
"description": "Value of environment variable",
"x-example": "production"
}
},
"required": [
"name",
"value"
],
"example": {
"name": "NODE_ENV",
"value": "production"
}
},
"vcsContent": {
"description": "VcsContents",
"type": "object",
+29 -10
View File
@@ -7222,7 +7222,8 @@
"default": {
"type": "string",
"description": "Default value for the attribute in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. Cannot be set when attribute is required.",
"x-example": null
"x-example": null,
"x-nullable": true
},
"array": {
"type": "boolean",
@@ -13212,7 +13213,7 @@
"branch",
"tag"
],
"x-enum-name": null,
"x-enum-name": "TemplateReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -13313,7 +13314,7 @@
"branch",
"commit"
],
"x-enum-name": "VCSDeploymentType",
"x-enum-name": "VCSReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -22860,7 +22861,7 @@
"commit",
"tag"
],
"x-enum-name": null,
"x-enum-name": "TemplateReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -22962,7 +22963,7 @@
"commit",
"tag"
],
"x-enum-name": "VCSDeploymentType",
"x-enum-name": "VCSReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -23937,7 +23938,7 @@
"parameters": [
{
"name": "queries",
"description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: enabled, name, fileSecurity, maximumFileSize, encryption, antivirus",
"description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: enabled, name, fileSecurity, maximumFileSize, encryption, antivirus, transformations",
"required": false,
"schema": {
"type": "array",
@@ -24088,6 +24089,11 @@
"type": "boolean",
"description": "Is virus scanning enabled? For file size above 20MB AntiVirus scanning is skipped even if it's enabled",
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Are image transformations enabled?",
"x-example": false
}
},
"required": [
@@ -24284,6 +24290,11 @@
"type": "boolean",
"description": "Is virus scanning enabled? For file size above 20MB AntiVirus scanning is skipped even if it's enabled",
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Are image transformations enabled?",
"x-example": false
}
},
"required": [
@@ -26368,7 +26379,7 @@
},
"rowSecurity": {
"type": "boolean",
"description": "Enables configuring permissions for individual rows. A user needs one of row or table level permissions to access a document. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"description": "Enables configuring permissions for individual rows. A user needs one of row or table-level permissions to access a row. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"x-example": false
},
"enabled": {
@@ -26870,7 +26881,8 @@
"default": {
"type": "string",
"description": "Default value for the column in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. Cannot be set when column is required.",
"x-example": null
"x-example": null,
"x-nullable": true
},
"array": {
"type": "boolean",
@@ -42282,6 +42294,11 @@
"type": "boolean",
"description": "Virus scanning is enabled.",
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Image transformations are enabled.",
"x-example": false
}
},
"required": [
@@ -42296,7 +42313,8 @@
"allowedFileExtensions",
"compression",
"encryption",
"antivirus"
"antivirus",
"transformations"
],
"example": {
"$id": "5e5ea5c16897e",
@@ -42315,7 +42333,8 @@
],
"compression": "gzip",
"encryption": false,
"antivirus": false
"antivirus": false,
"transformations": false
}
},
"resourceToken": {
+161 -18
View File
@@ -7851,7 +7851,8 @@
"type": "string",
"description": "Default value for the attribute in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. Cannot be set when attribute is required.",
"default": null,
"x-example": null
"x-example": null,
"x-nullable": true
},
"array": {
"type": "boolean",
@@ -14355,11 +14356,24 @@
"default": null,
"x-example": "<ROOT_DIRECTORY>"
},
"version": {
"type": {
"type": "string",
"description": "Version (tag) for the repo linked to the function template.",
"description": "Type for the reference provided. Can be commit, branch, or tag",
"default": null,
"x-example": "<VERSION>"
"x-example": "commit",
"enum": [
"commit",
"branch",
"tag"
],
"x-enum-name": "TemplateReferenceType",
"x-enum-keys": []
},
"reference": {
"type": "string",
"description": "Reference value, can be a commit hash, branch name, or release tag",
"default": null,
"x-example": "<REFERENCE>"
},
"activate": {
"type": "boolean",
@@ -14372,7 +14386,8 @@
"repository",
"owner",
"rootDirectory",
"version"
"type",
"reference"
]
}
}
@@ -14452,7 +14467,7 @@
"branch",
"commit"
],
"x-enum-name": "VCSDeploymentType",
"x-enum-name": "VCSReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -32416,11 +32431,24 @@
"default": null,
"x-example": "<ROOT_DIRECTORY>"
},
"version": {
"type": {
"type": "string",
"description": "Version (tag) for the repo linked to the site template.",
"description": "Type for the reference provided. Can be commit, branch, or tag",
"default": null,
"x-example": "<VERSION>"
"x-example": "branch",
"enum": [
"branch",
"commit",
"tag"
],
"x-enum-name": "TemplateReferenceType",
"x-enum-keys": []
},
"reference": {
"type": "string",
"description": "Reference value, can be a commit hash, branch name, or release tag",
"default": null,
"x-example": "<REFERENCE>"
},
"activate": {
"type": "boolean",
@@ -32433,7 +32461,8 @@
"repository",
"owner",
"rootDirectory",
"version"
"type",
"reference"
]
}
}
@@ -32514,7 +32543,7 @@
"commit",
"tag"
],
"x-enum-name": "VCSDeploymentType",
"x-enum-name": "VCSReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -33536,7 +33565,7 @@
"parameters": [
{
"name": "queries",
"description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: enabled, name, fileSecurity, maximumFileSize, encryption, antivirus",
"description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: enabled, name, fileSecurity, maximumFileSize, encryption, antivirus, transformations",
"required": false,
"type": "array",
"collectionFormat": "multi",
@@ -33694,6 +33723,12 @@
"description": "Is virus scanning enabled? For file size above 20MB AntiVirus scanning is skipped even if it's enabled",
"default": true,
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Are image transformations enabled?",
"default": true,
"x-example": false
}
},
"required": [
@@ -33893,6 +33928,12 @@
"description": "Is virus scanning enabled? For file size above 20MB AntiVirus scanning is skipped even if it's enabled",
"default": true,
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Are image transformations enabled?",
"default": true,
"x-example": false
}
},
"required": [
@@ -36122,7 +36163,7 @@
},
"rowSecurity": {
"type": "boolean",
"description": "Enables configuring permissions for individual rows. A user needs one of row or table level permissions to access a document. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"description": "Enables configuring permissions for individual rows. A user needs one of row or table-level permissions to access a row. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"default": false,
"x-example": false
},
@@ -36610,7 +36651,8 @@
"type": "string",
"description": "Default value for the column in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. Cannot be set when column is required.",
"default": null,
"x-example": null
"x-example": null,
"x-nullable": true
},
"array": {
"type": "boolean",
@@ -53397,6 +53439,11 @@
"type": "boolean",
"description": "Virus scanning is enabled.",
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Image transformations are enabled.",
"x-example": false
}
},
"required": [
@@ -53411,7 +53458,8 @@
"allowedFileExtensions",
"compression",
"encryption",
"antivirus"
"antivirus",
"transformations"
],
"example": {
"$id": "5e5ea5c16897e",
@@ -53430,7 +53478,8 @@
],
"compression": "gzip",
"encryption": false,
"antivirus": false
"antivirus": false,
"transformations": false
}
},
"resourceToken": {
@@ -54623,6 +54672,17 @@
"type": "string",
"description": "Last commit date in ISO 8601 format.",
"x-example": "datetime"
},
"variables": {
"type": "array",
"description": "Environment variables found in .env files",
"items": {
"type": "string"
},
"x-example": [
"PORT",
"NODE_ENV"
]
}
},
"required": [
@@ -54632,7 +54692,8 @@
"provider",
"private",
"defaultBranch",
"pushedAt"
"pushedAt",
"variables"
],
"example": {
"id": "5e5ea5c16897e",
@@ -54641,7 +54702,11 @@
"provider": "github",
"private": true,
"defaultBranch": "main",
"pushedAt": "datetime"
"pushedAt": "datetime",
"variables": [
"PORT",
"NODE_ENV"
]
}
},
"providerRepositoryFramework": {
@@ -54683,6 +54748,17 @@
"description": "Last commit date in ISO 8601 format.",
"x-example": "datetime"
},
"variables": {
"type": "array",
"description": "Environment variables found in .env files",
"items": {
"type": "string"
},
"x-example": [
"PORT",
"NODE_ENV"
]
},
"framework": {
"type": "string",
"description": "Auto-detected framework. Empty if type is not \"framework\".",
@@ -54697,6 +54773,7 @@
"private",
"defaultBranch",
"pushedAt",
"variables",
"framework"
],
"example": {
@@ -54707,6 +54784,10 @@
"private": true,
"defaultBranch": "main",
"pushedAt": "datetime",
"variables": [
"PORT",
"NODE_ENV"
],
"framework": "nextjs"
}
},
@@ -54749,6 +54830,17 @@
"description": "Last commit date in ISO 8601 format.",
"x-example": "datetime"
},
"variables": {
"type": "array",
"description": "Environment variables found in .env files",
"items": {
"type": "string"
},
"x-example": [
"PORT",
"NODE_ENV"
]
},
"runtime": {
"type": "string",
"description": "Auto-detected runtime. Empty if type is not \"runtime\".",
@@ -54763,6 +54855,7 @@
"private",
"defaultBranch",
"pushedAt",
"variables",
"runtime"
],
"example": {
@@ -54773,6 +54866,10 @@
"private": true,
"defaultBranch": "main",
"pushedAt": "datetime",
"variables": [
"PORT",
"NODE_ENV"
],
"runtime": "node-22"
}
},
@@ -54780,6 +54877,16 @@
"description": "DetectionFramework",
"type": "object",
"properties": {
"variables": {
"type": "array",
"description": "Environment variables found in .env files",
"items": {
"type": "object",
"$ref": "#\/definitions\/detectionVariable"
},
"x-example": {},
"x-nullable": true
},
"framework": {
"type": "string",
"description": "Framework",
@@ -54808,6 +54915,7 @@
"outputDirectory"
],
"example": {
"variables": {},
"framework": "nuxt",
"installCommand": "npm install",
"buildCommand": "npm run build",
@@ -54818,6 +54926,16 @@
"description": "DetectionRuntime",
"type": "object",
"properties": {
"variables": {
"type": "array",
"description": "Environment variables found in .env files",
"items": {
"type": "object",
"$ref": "#\/definitions\/detectionVariable"
},
"x-example": {},
"x-nullable": true
},
"runtime": {
"type": "string",
"description": "Runtime",
@@ -54840,11 +54958,36 @@
"commands"
],
"example": {
"variables": {},
"runtime": "node",
"entrypoint": "index.js",
"commands": "npm install && npm run build"
}
},
"detectionVariable": {
"description": "DetectionVariable",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of environment variable",
"x-example": "NODE_ENV"
},
"value": {
"type": "string",
"description": "Value of environment variable",
"x-example": "production"
}
},
"required": [
"name",
"value"
],
"example": {
"name": "NODE_ENV",
"value": "production"
}
},
"vcsContent": {
"description": "VcsContents",
"type": "object",
+65 -16
View File
@@ -7310,7 +7310,8 @@
"type": "string",
"description": "Default value for the attribute in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. Cannot be set when attribute is required.",
"default": null,
"x-example": null
"x-example": null,
"x-nullable": true
},
"array": {
"type": "boolean",
@@ -13171,11 +13172,24 @@
"default": null,
"x-example": "<ROOT_DIRECTORY>"
},
"version": {
"type": {
"type": "string",
"description": "Version (tag) for the repo linked to the function template.",
"description": "Type for the reference provided. Can be commit, branch, or tag",
"default": null,
"x-example": "<VERSION>"
"x-example": "commit",
"enum": [
"commit",
"branch",
"tag"
],
"x-enum-name": "TemplateReferenceType",
"x-enum-keys": []
},
"reference": {
"type": "string",
"description": "Reference value, can be a commit hash, branch name, or release tag",
"default": null,
"x-example": "<REFERENCE>"
},
"activate": {
"type": "boolean",
@@ -13188,7 +13202,8 @@
"repository",
"owner",
"rootDirectory",
"version"
"type",
"reference"
]
}
}
@@ -13269,7 +13284,7 @@
"branch",
"commit"
],
"x-enum-name": "VCSDeploymentType",
"x-enum-name": "VCSReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -22989,11 +23004,24 @@
"default": null,
"x-example": "<ROOT_DIRECTORY>"
},
"version": {
"type": {
"type": "string",
"description": "Version (tag) for the repo linked to the site template.",
"description": "Type for the reference provided. Can be commit, branch, or tag",
"default": null,
"x-example": "<VERSION>"
"x-example": "branch",
"enum": [
"branch",
"commit",
"tag"
],
"x-enum-name": "TemplateReferenceType",
"x-enum-keys": []
},
"reference": {
"type": "string",
"description": "Reference value, can be a commit hash, branch name, or release tag",
"default": null,
"x-example": "<REFERENCE>"
},
"activate": {
"type": "boolean",
@@ -23006,7 +23034,8 @@
"repository",
"owner",
"rootDirectory",
"version"
"type",
"reference"
]
}
}
@@ -23088,7 +23117,7 @@
"commit",
"tag"
],
"x-enum-name": "VCSDeploymentType",
"x-enum-name": "VCSReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -24045,7 +24074,7 @@
"parameters": [
{
"name": "queries",
"description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: enabled, name, fileSecurity, maximumFileSize, encryption, antivirus",
"description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: enabled, name, fileSecurity, maximumFileSize, encryption, antivirus, transformations",
"required": false,
"type": "array",
"collectionFormat": "multi",
@@ -24204,6 +24233,12 @@
"description": "Is virus scanning enabled? For file size above 20MB AntiVirus scanning is skipped even if it's enabled",
"default": true,
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Are image transformations enabled?",
"default": true,
"x-example": false
}
},
"required": [
@@ -24405,6 +24440,12 @@
"description": "Is virus scanning enabled? For file size above 20MB AntiVirus scanning is skipped even if it's enabled",
"default": true,
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Are image transformations enabled?",
"default": true,
"x-example": false
}
},
"required": [
@@ -26429,7 +26470,7 @@
},
"rowSecurity": {
"type": "boolean",
"description": "Enables configuring permissions for individual rows. A user needs one of row or table level permissions to access a document. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"description": "Enables configuring permissions for individual rows. A user needs one of row or table-level permissions to access a row. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"default": false,
"x-example": false
},
@@ -26922,7 +26963,8 @@
"type": "string",
"description": "Default value for the column in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. Cannot be set when column is required.",
"default": null,
"x-example": null
"x-example": null,
"x-nullable": true
},
"array": {
"type": "boolean",
@@ -42175,6 +42217,11 @@
"type": "boolean",
"description": "Virus scanning is enabled.",
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Image transformations are enabled.",
"x-example": false
}
},
"required": [
@@ -42189,7 +42236,8 @@
"allowedFileExtensions",
"compression",
"encryption",
"antivirus"
"antivirus",
"transformations"
],
"example": {
"$id": "5e5ea5c16897e",
@@ -42208,7 +42256,8 @@
],
"compression": "gzip",
"encryption": false,
"antivirus": false
"antivirus": false,
"transformations": false
}
},
"resourceToken": {
+127 -12
View File
@@ -7851,7 +7851,8 @@
"type": "string",
"description": "Default value for the attribute in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. Cannot be set when attribute is required.",
"default": null,
"x-example": null
"x-example": null,
"x-nullable": true
},
"array": {
"type": "boolean",
@@ -14365,7 +14366,7 @@
"branch",
"tag"
],
"x-enum-name": null,
"x-enum-name": "TemplateReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -14466,7 +14467,7 @@
"branch",
"commit"
],
"x-enum-name": "VCSDeploymentType",
"x-enum-name": "VCSReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -32440,7 +32441,7 @@
"commit",
"tag"
],
"x-enum-name": null,
"x-enum-name": "TemplateReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -32542,7 +32543,7 @@
"commit",
"tag"
],
"x-enum-name": "VCSDeploymentType",
"x-enum-name": "VCSReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -33564,7 +33565,7 @@
"parameters": [
{
"name": "queries",
"description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: enabled, name, fileSecurity, maximumFileSize, encryption, antivirus",
"description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: enabled, name, fileSecurity, maximumFileSize, encryption, antivirus, transformations",
"required": false,
"type": "array",
"collectionFormat": "multi",
@@ -33722,6 +33723,12 @@
"description": "Is virus scanning enabled? For file size above 20MB AntiVirus scanning is skipped even if it's enabled",
"default": true,
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Are image transformations enabled?",
"default": true,
"x-example": false
}
},
"required": [
@@ -33921,6 +33928,12 @@
"description": "Is virus scanning enabled? For file size above 20MB AntiVirus scanning is skipped even if it's enabled",
"default": true,
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Are image transformations enabled?",
"default": true,
"x-example": false
}
},
"required": [
@@ -36150,7 +36163,7 @@
},
"rowSecurity": {
"type": "boolean",
"description": "Enables configuring permissions for individual rows. A user needs one of row or table level permissions to access a document. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"description": "Enables configuring permissions for individual rows. A user needs one of row or table-level permissions to access a row. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"default": false,
"x-example": false
},
@@ -36638,7 +36651,8 @@
"type": "string",
"description": "Default value for the column in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. Cannot be set when column is required.",
"default": null,
"x-example": null
"x-example": null,
"x-nullable": true
},
"array": {
"type": "boolean",
@@ -53425,6 +53439,11 @@
"type": "boolean",
"description": "Virus scanning is enabled.",
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Image transformations are enabled.",
"x-example": false
}
},
"required": [
@@ -53439,7 +53458,8 @@
"allowedFileExtensions",
"compression",
"encryption",
"antivirus"
"antivirus",
"transformations"
],
"example": {
"$id": "5e5ea5c16897e",
@@ -53458,7 +53478,8 @@
],
"compression": "gzip",
"encryption": false,
"antivirus": false
"antivirus": false,
"transformations": false
}
},
"resourceToken": {
@@ -54651,6 +54672,17 @@
"type": "string",
"description": "Last commit date in ISO 8601 format.",
"x-example": "datetime"
},
"variables": {
"type": "array",
"description": "Environment variables found in .env files",
"items": {
"type": "string"
},
"x-example": [
"PORT",
"NODE_ENV"
]
}
},
"required": [
@@ -54660,7 +54692,8 @@
"provider",
"private",
"defaultBranch",
"pushedAt"
"pushedAt",
"variables"
],
"example": {
"id": "5e5ea5c16897e",
@@ -54669,7 +54702,11 @@
"provider": "github",
"private": true,
"defaultBranch": "main",
"pushedAt": "datetime"
"pushedAt": "datetime",
"variables": [
"PORT",
"NODE_ENV"
]
}
},
"providerRepositoryFramework": {
@@ -54711,6 +54748,17 @@
"description": "Last commit date in ISO 8601 format.",
"x-example": "datetime"
},
"variables": {
"type": "array",
"description": "Environment variables found in .env files",
"items": {
"type": "string"
},
"x-example": [
"PORT",
"NODE_ENV"
]
},
"framework": {
"type": "string",
"description": "Auto-detected framework. Empty if type is not \"framework\".",
@@ -54725,6 +54773,7 @@
"private",
"defaultBranch",
"pushedAt",
"variables",
"framework"
],
"example": {
@@ -54735,6 +54784,10 @@
"private": true,
"defaultBranch": "main",
"pushedAt": "datetime",
"variables": [
"PORT",
"NODE_ENV"
],
"framework": "nextjs"
}
},
@@ -54777,6 +54830,17 @@
"description": "Last commit date in ISO 8601 format.",
"x-example": "datetime"
},
"variables": {
"type": "array",
"description": "Environment variables found in .env files",
"items": {
"type": "string"
},
"x-example": [
"PORT",
"NODE_ENV"
]
},
"runtime": {
"type": "string",
"description": "Auto-detected runtime. Empty if type is not \"runtime\".",
@@ -54791,6 +54855,7 @@
"private",
"defaultBranch",
"pushedAt",
"variables",
"runtime"
],
"example": {
@@ -54801,6 +54866,10 @@
"private": true,
"defaultBranch": "main",
"pushedAt": "datetime",
"variables": [
"PORT",
"NODE_ENV"
],
"runtime": "node-22"
}
},
@@ -54808,6 +54877,16 @@
"description": "DetectionFramework",
"type": "object",
"properties": {
"variables": {
"type": "array",
"description": "Environment variables found in .env files",
"items": {
"type": "object",
"$ref": "#\/definitions\/detectionVariable"
},
"x-example": {},
"x-nullable": true
},
"framework": {
"type": "string",
"description": "Framework",
@@ -54836,6 +54915,7 @@
"outputDirectory"
],
"example": {
"variables": {},
"framework": "nuxt",
"installCommand": "npm install",
"buildCommand": "npm run build",
@@ -54846,6 +54926,16 @@
"description": "DetectionRuntime",
"type": "object",
"properties": {
"variables": {
"type": "array",
"description": "Environment variables found in .env files",
"items": {
"type": "object",
"$ref": "#\/definitions\/detectionVariable"
},
"x-example": {},
"x-nullable": true
},
"runtime": {
"type": "string",
"description": "Runtime",
@@ -54868,11 +54958,36 @@
"commands"
],
"example": {
"variables": {},
"runtime": "node",
"entrypoint": "index.js",
"commands": "npm install && npm run build"
}
},
"detectionVariable": {
"description": "DetectionVariable",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of environment variable",
"x-example": "NODE_ENV"
},
"value": {
"type": "string",
"description": "Value of environment variable",
"x-example": "production"
}
},
"required": [
"name",
"value"
],
"example": {
"name": "NODE_ENV",
"value": "production"
}
},
"vcsContent": {
"description": "VcsContents",
"type": "object",
+31 -10
View File
@@ -7310,7 +7310,8 @@
"type": "string",
"description": "Default value for the attribute in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. Cannot be set when attribute is required.",
"default": null,
"x-example": null
"x-example": null,
"x-nullable": true
},
"array": {
"type": "boolean",
@@ -13181,7 +13182,7 @@
"branch",
"tag"
],
"x-enum-name": null,
"x-enum-name": "TemplateReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -13283,7 +13284,7 @@
"branch",
"commit"
],
"x-enum-name": "VCSDeploymentType",
"x-enum-name": "VCSReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -23013,7 +23014,7 @@
"commit",
"tag"
],
"x-enum-name": null,
"x-enum-name": "TemplateReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -23116,7 +23117,7 @@
"commit",
"tag"
],
"x-enum-name": "VCSDeploymentType",
"x-enum-name": "VCSReferenceType",
"x-enum-keys": []
},
"reference": {
@@ -24073,7 +24074,7 @@
"parameters": [
{
"name": "queries",
"description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: enabled, name, fileSecurity, maximumFileSize, encryption, antivirus",
"description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: enabled, name, fileSecurity, maximumFileSize, encryption, antivirus, transformations",
"required": false,
"type": "array",
"collectionFormat": "multi",
@@ -24232,6 +24233,12 @@
"description": "Is virus scanning enabled? For file size above 20MB AntiVirus scanning is skipped even if it's enabled",
"default": true,
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Are image transformations enabled?",
"default": true,
"x-example": false
}
},
"required": [
@@ -24433,6 +24440,12 @@
"description": "Is virus scanning enabled? For file size above 20MB AntiVirus scanning is skipped even if it's enabled",
"default": true,
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Are image transformations enabled?",
"default": true,
"x-example": false
}
},
"required": [
@@ -26457,7 +26470,7 @@
},
"rowSecurity": {
"type": "boolean",
"description": "Enables configuring permissions for individual rows. A user needs one of row or table level permissions to access a document. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"description": "Enables configuring permissions for individual rows. A user needs one of row or table-level permissions to access a row. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"default": false,
"x-example": false
},
@@ -26950,7 +26963,8 @@
"type": "string",
"description": "Default value for the column in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. Cannot be set when column is required.",
"default": null,
"x-example": null
"x-example": null,
"x-nullable": true
},
"array": {
"type": "boolean",
@@ -42203,6 +42217,11 @@
"type": "boolean",
"description": "Virus scanning is enabled.",
"x-example": false
},
"transformations": {
"type": "boolean",
"description": "Image transformations are enabled.",
"x-example": false
}
},
"required": [
@@ -42217,7 +42236,8 @@
"allowedFileExtensions",
"compression",
"encryption",
"antivirus"
"antivirus",
"transformations"
],
"example": {
"$id": "5e5ea5c16897e",
@@ -42236,7 +42256,8 @@
],
"compression": "gzip",
"encryption": false,
"antivirus": false
"antivirus": false,
"transformations": false
}
},
"resourceToken": {
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,5 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\GraphQL\Promises\Adapter;
@@ -9,6 +8,7 @@ use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use GraphQL\Error\DebugFlag;
@@ -32,7 +32,7 @@ App::init()
if (
array_key_exists('graphql', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['graphql']
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
+1 -2
View File
@@ -1,7 +1,6 @@
<?php
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\MockNumber;
use Appwrite\Event\Delete;
use Appwrite\Event\Mail;
@@ -119,7 +118,7 @@ App::post('/v1/projects')
'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT,
'passwordHistory' => 0,
'passwordDictionary' => false,
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG,
'duration' => TOKEN_EXPIRATION_LOGIN_LONG,
'personalDataCheck' => false,
'mockNumbers' => [],
'sessionAlerts' => false,
+45 -27
View File
@@ -2,7 +2,6 @@
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Auth;
use Appwrite\ClamAV\Network;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
@@ -13,6 +12,7 @@ use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries\Buckets;
use Appwrite\Utopia\Database\Validator\Queries\Files;
@@ -86,10 +86,11 @@ App::post('/v1/storage/buckets')
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD], true), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
->param('transformations', true, new Boolean(true), 'Are image transformations enabled?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $queueForEvents) {
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, bool $transformations, Response $response, Database $dbForProject, Event $queueForEvents) {
$bucketId = $bucketId === 'unique()' ? ID::unique() : $bucketId;
@@ -142,6 +143,7 @@ App::post('/v1/storage/buckets')
'compression' => $compression,
'encryption' => $encryption,
'antivirus' => $antivirus,
'transformations' => $transformations,
'search' => implode(' ', [$bucketId, $name]),
]));
@@ -299,10 +301,11 @@ App::put('/v1/storage/buckets/:bucketId')
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD], true), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
->param('transformations', true, new Boolean(true), 'Are image transformations enabled?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $queueForEvents) {
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, bool $transformations, Response $response, Database $dbForProject, Event $queueForEvents) {
$bucket = $dbForProject->getDocument('buckets', $bucketId);
if ($bucket->isEmpty()) {
@@ -316,6 +319,7 @@ App::put('/v1/storage/buckets/:bucketId')
$encryption ??= $bucket->getAttribute('encryption', true);
$antivirus ??= $bucket->getAttribute('antivirus', true);
$compression ??= $bucket->getAttribute('compression', Compression::NONE);
$transformations ??= $bucket->getAttribute('transformations', true);
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions);
@@ -329,7 +333,8 @@ App::put('/v1/storage/buckets/:bucketId')
->setAttribute('enabled', $enabled)
->setAttribute('encryption', $encryption)
->setAttribute('compression', $compression)
->setAttribute('antivirus', $antivirus));
->setAttribute('antivirus', $antivirus)
->setAttribute('transformations', $transformations));
$dbForProject->updateCollection('bucket_' . $bucket->getSequence(), $permissions, $fileSecurity);
@@ -432,8 +437,8 @@ App::post('/v1/storage/buckets/:bucketId/files')
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -465,7 +470,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();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
if (!User::isApp($roles) && !User::isPrivileged($roles)) {
foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) {
$permission = Permission::parse($permission);
@@ -676,7 +681,13 @@ App::post('/v1/storage/buckets/:bucketId/files')
'metadata' => $metadata,
]);
$file = $dbForProject->createDocument('bucket_' . $bucket->getSequence(), $doc);
try {
$file = $dbForProject->createDocument('bucket_' . $bucket->getSequence(), $doc);
} catch (DuplicateException) {
throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS);
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
} else {
$file = $file
->setAttribute('$permissions', $permissions)
@@ -726,6 +737,8 @@ App::post('/v1/storage/buckets/:bucketId/files')
try {
$file = $dbForProject->createDocument('bucket_' . $bucket->getSequence(), $doc);
} catch (DuplicateException) {
throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS);
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
@@ -794,8 +807,8 @@ App::get('/v1/storage/buckets/:bucketId/files')
->action(function (string $bucketId, array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject, string $mode) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -895,8 +908,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId')
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, string $mode) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -977,13 +990,17 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
/* @type Document $bucket */
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
if (!$bucket->getAttribute('transformations', true) && !$isAPIKey && !$isPrivilegedUser) {
throw new Exception(Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED);
}
$isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_READ);
@@ -1118,7 +1135,7 @@ 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 (!User::isPrivileged(Authorization::getRoles())) {
$transformedAt = $file->getAttribute('transformedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
$file->setAttribute('transformedAt', DateTime::now());
@@ -1169,8 +1186,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
/* @type Document $bucket */
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -1330,8 +1347,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
/* @type Document $bucket */
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -1500,10 +1517,11 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
}
$isInternal = $decoded['internal'] ?? false;
$disposition = $decoded['disposition'] ?? 'inline';
$dbForProject = $isInternal ? $dbForPlatform : $dbForProject;
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
@@ -1556,7 +1574,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
->setContentType($contentType)
->addHeader('Content-Security-Policy', 'script-src none;')
->addHeader('X-Content-Type-Options', 'nosniff')
->addHeader('Content-Disposition', 'inline; filename="' . $file->getAttribute('name', '') . '"')
->addHeader('Content-Disposition', $disposition . '; filename="' . $file->getAttribute('name', '') . '"')
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
->addHeader('X-Peak', \memory_get_peak_usage());
@@ -1659,8 +1677,8 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -1689,7 +1707,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();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles) && !\is_null($permissions)) {
if (!User::isApp($roles) && !User::isPrivileged($roles) && !\is_null($permissions)) {
foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) {
$permission = Permission::parse($permission);
@@ -1773,8 +1791,8 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
->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));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
+48 -36
View File
@@ -1,6 +1,5 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
@@ -18,6 +17,7 @@ use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Template\Template;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries\Memberships;
use Appwrite\Utopia\Database\Validator\Queries\Teams;
@@ -28,6 +28,9 @@ 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;
@@ -87,8 +90,8 @@ App::post('/v1/teams')
->inject('queueForEvents')
->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$isAppUser = User::isApp(Authorization::getRoles());
$teamId = $teamId == 'unique()' ? ID::unique() : $teamId;
@@ -176,6 +179,7 @@ App::get('/v1/teams')
->inject('dbForProject')
->action(function (array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@@ -474,10 +478,9 @@ App::post('/v1/teams/:teamId/memberships')
->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', []));
array_filter($roles, function ($role) {
return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]);
$roles = array_filter($roles, function ($role) {
return !in_array($role, [User::ROLE_APPS, User::ROLE_GUESTS, User::ROLE_USERS]);
});
return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE);
}
@@ -496,9 +499,11 @@ App::post('/v1/teams/:teamId/memberships')
->inject('timelimit')
->inject('queueForStatsUsage')
->inject('plan')
->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) {
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
->inject('proofForPassword')
->inject('proofForToken')
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Password $proofForPassword, Token $proofForToken) {
$isAppUser = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$url = htmlentities($url);
if (empty($url)) {
@@ -568,6 +573,8 @@ App::post('/v1/teams/:teamId/memberships')
}
try {
$userId = ID::unique();
$hash = $proofForPassword->hash($proofForPassword->generate());
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
@@ -588,9 +595,9 @@ App::post('/v1/teams/:teamId/memberships')
'emailVerification' => false,
'status' => true,
// TODO: Set password empty?
'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),
'hash' => Auth::DEFAULT_ALGO,
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
'password' => $hash,
'hash' => $proofForPassword->getHash()->getName(),
'hashOptions' => $proofForPassword->getHash()->getOptions(),
/**
* Set the password update time to 0 for users created using
* team invite and OAuth to allow password updates without an
@@ -630,7 +637,7 @@ App::post('/v1/teams/:teamId/memberships')
Query::equal('teamInternalId', [$team->getSequence()]),
]);
$secret = Auth::tokenGenerator();
$secret = $proofForToken->generate();
if ($membership->isEmpty()) {
$membershipId = ID::unique();
$membership = new Document([
@@ -650,7 +657,7 @@ App::post('/v1/teams/:teamId/memberships')
'invited' => DateTime::now(),
'joined' => ($isPrivilegedUser || $isAppUser) ? DateTime::now() : null,
'confirm' => ($isPrivilegedUser || $isAppUser),
'secret' => Auth::hash($secret),
'secret' => $proofForToken->hash($secret),
'search' => implode(' ', [$membershipId, $invitee->getId()])
]);
@@ -661,9 +668,8 @@ App::post('/v1/teams/:teamId/memberships')
if ($isPrivilegedUser || $isAppUser) {
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
}
} elseif ($membership->getAttribute('confirm') === false) {
$membership->setAttribute('secret', Auth::hash($secret));
$membership->setAttribute('secret', $proofForToken->hash($secret));
$membership->setAttribute('invited', DateTime::now());
if ($isPrivilegedUser || $isAppUser) {
@@ -766,7 +772,6 @@ App::post('/v1/teams/:teamId/memberships')
->setName($invitee->getAttribute('name', ''))
->setVariables($emailVariables)
->trigger();
} elseif (!empty($phone)) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
@@ -930,8 +935,8 @@ App::get('/v1/teams/:teamId/memberships')
];
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
$isPrivilegedUser = User::isPrivileged($roles);
$isAppUser = User::isApp($roles);
$membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) {
return $privacy || $isPrivilegedUser || $isAppUser;
@@ -1021,8 +1026,8 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
];
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
$isPrivilegedUser = User::isPrivileged($roles);
$isAppUser = User::isApp($roles);
$membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) {
return $privacy || $isPrivilegedUser || $isAppUser;
@@ -1087,8 +1092,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
->param('roles', [], function (Document $project) {
if ($project->getId() === 'console') {
$roles = array_keys(Config::getParam('roles', []));
array_filter($roles, function ($role) {
return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]);
$roles = array_filter($roles, function ($role) {
return !in_array($role, [User::ROLE_APPS, User::ROLE_GUESTS, User::ROLE_USERS]);
});
return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE);
}
@@ -1117,8 +1122,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
throw new Exception(Exception::USER_NOT_FOUND);
}
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$isAppUser = User::isApp(Authorization::getRoles());
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');
if ($project->getId() === 'console') {
@@ -1203,7 +1208,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->inject('project')
->inject('geodb')
->inject('queueForEvents')
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $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) {
$protocol = $request->getProtocol();
$membership = $dbForProject->getDocument('memberships', $membershipId);
@@ -1222,7 +1229,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
throw new Exception(Exception::TEAM_MEMBERSHIP_MISMATCH);
}
if (Auth::hash($secret) !== $membership->getAttribute('secret')) {
if (!$proofForToken->verify($secret, $membership->getAttribute('secret'))) {
throw new Exception(Exception::TEAM_INVALID_SECRET);
}
@@ -1256,9 +1263,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$authDuration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::addSeconds(new \DateTime(), $authDuration);
$secret = Auth::tokenGenerator();
$secret = $proofForToken->generate();
$session = new Document(array_merge([
'$id' => ID::unique(),
'$permissions' => [
@@ -1268,9 +1275,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
],
'userId' => $user->getId(),
'userInternalId' => $user->getSequence(),
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'provider' => SESSION_PROVIDER_EMAIL,
'providerUid' => $user->getAttribute('email'),
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'factors' => ['email'],
@@ -1282,14 +1289,19 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
Authorization::setRole(Role::user($userId)->toString());
$encoded = $store
->setProperty('id', $user->getId())
->setProperty('secret', $secret)
->encode();
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]));
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
}
$response
->addCookie(
name: Auth::$cookieName . '_legacy',
value: Auth::encodeSession($user->getId(), $secret),
name: $store->getKey() . '_legacy',
value: $encoded,
expire: (new \DateTime($expire))->getTimestamp(),
path: '/',
domain: Config::getParam('cookieDomain'),
@@ -1297,8 +1309,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
httponly: true
)
->addCookie(
name: Auth::$cookieName,
value: Auth::encodeSession($user->getId(), $secret),
name: $store->getKey(),
value: $encoded,
expire: (new \DateTime($expire))->getTimestamp(),
path: '/',
domain: Config::getParam('cookieDomain'),
+100 -76
View File
@@ -1,7 +1,6 @@
<?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;
@@ -32,6 +31,18 @@ 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;
@@ -62,10 +73,9 @@ use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
/** TODO: Remove function when we move to using utopia/platform */
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
function createUser(Hash $hash, 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)) {
@@ -104,8 +114,24 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
} catch (Throwable) {
$emailCanonical = null;
}
$hashedPassword = null;
$isHashed = !$hash instanceof Plaintext;
$defaultHash = new ProofsPassword();
if (!empty($password)) {
if (!$isHashed) { // Password was never hashed, hash it with the default hash
$hashedPassword = $defaultHash->hash($password);
$hash = $defaultHash->getHash();
} else {
$hashedPassword = $password;
}
} else {
// when password is not provided, plaintext was set as the default hash causing the issue
$hash = $defaultHash->getHash();
$isHashed = !$hash instanceof Plaintext;
}
$password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null;
$user = new Document([
'$id' => $userId,
'$permissions' => [
@@ -119,11 +145,11 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
'phoneVerification' => false,
'status' => true,
'labels' => [],
'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],
'password' => $hashedPassword,
'passwordHistory' => is_null($hashedPassword) || $passwordHistory === 0 ? [] : [$hashedPassword],
'passwordUpdate' => (!empty($hashedPassword)) ? DateTime::now() : null,
'hash' => $hash->getName(),
'hashOptions' => $hash->getOptions(),
'registration' => DateTime::now(),
'reset' => false,
'name' => $name,
@@ -139,7 +165,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
'emailIsFree' => $emailCanonical?->isFree(),
]);
if ($hash === 'plaintext') {
if (!$isHashed && !empty($password)) {
$hooks->trigger('passwordValidator', [$dbForProject, $project, $plaintextPassword, &$user, true]);
}
@@ -230,7 +256,9 @@ 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) {
$user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks);
$plaintext = new Plaintext();
$user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($user, Response::MODEL_USER);
@@ -264,7 +292,10 @@ 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) {
$user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$bcrypt = new Bcrypt();
$bcrypt->setCost(8); // Default cost
$user = createUser($bcrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -299,7 +330,9 @@ 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) {
$user = createUser('md5', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$md5 = new MD5();
$user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -334,7 +367,9 @@ 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) {
$user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$argon2 = new Argon2();
$user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -370,13 +405,12 @@ 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) {
$options = '{}';
$sha = new Sha();
if (!empty($passwordVersion)) {
$options = '{"version":"' . $passwordVersion . '"}';
$sha->setVersion($passwordVersion);
}
$user = createUser('sha', $options, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser($sha, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -411,7 +445,9 @@ 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) {
$user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$phpass = new PHPass();
$user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -451,15 +487,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) {
$options = [
'salt' => $passwordSalt,
'costCpu' => $passwordCpu,
'costMemory' => $passwordMemory,
'costParallel' => $passwordParallel,
'length' => $passwordLength
];
$scrypt = new Scrypt();
$scrypt
->setSalt($passwordSalt)
->setCpuCost($passwordCpu)
->setMemoryCost($passwordMemory)
->setParallelCost($passwordParallel)
->setLength($passwordLength);
$user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser($scrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -497,7 +533,13 @@ 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) {
$user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$scryptModified = new ScryptModified();
$scryptModified
->setSalt($passwordSalt)
->setSaltSeparator($passwordSaltSeparator)
->setSignerKey($passwordSignerKey);
$user = createUser($scryptModified, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -809,16 +851,12 @@ App::get('/v1/users/:userId/sessions')
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$sessions = $user->getAttribute('sessions', []);
foreach ($sessions as $key => $session) {
/** @var Document $session */
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
$session->setAttribute('countryName', $countryName);
$session->setAttribute('current', false);
$sessions[$key] = $session;
}
@@ -858,28 +896,22 @@ App::get('/v1/users/:userId/memberships')
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
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);
}
// Set internal queries
$queries[] = Query::equal('userInternalId', [$user->getSequence()]);
$memberships = array_map(function ($membership) use ($dbForProject, $user) {
$team = $dbForProject->getDocument('teams', $membership->getAttribute('teamId'));
$membership
->setAttribute('teamName', $team->getAttribute('name'))
->setAttribute('userName', $user->getAttribute('name'))
->setAttribute('userEmail', $user->getAttribute('email'));
return $membership;
}, $dbForProject->find('memberships', $queries));
@@ -920,35 +952,26 @@ App::get('/v1/users/:userId/logs')
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Temp fix for logs
$queries[] = Query::or([
Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))),
Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))),
]);
$audit = new Audit($dbForProject);
$logs = $audit->getLogsByUser($user->getSequence(), $queries);
$output = [];
foreach ($logs as $i => &$log) {
$log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN';
$detector = new Detector($log['userAgent']);
$detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
$os = $detector->getOS();
$client = $detector->getClient();
$device = $detector->getDevice();
$output[$i] = new Document([
'event' => $log['event'],
'userId' => ID::custom($log['data']['userId']),
@@ -969,9 +992,7 @@ App::get('/v1/users/:userId/logs')
'deviceBrand' => $device['deviceBrand'],
'deviceModel' => $device['deviceModel']
]);
$record = $geodb->get($log['ip']);
if ($record) {
$output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--';
$output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
@@ -1015,15 +1036,12 @@ App::get('/v1/users/:userId/targets')
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$queries[] = Query::equal('userId', [$userId]);
/**
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
*/
@@ -1031,20 +1049,16 @@ App::get('/v1/users/:userId/targets')
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
});
$cursor = reset($cursor);
if ($cursor) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$targetId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('targets', $targetId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Target '{$targetId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
try {
@@ -1092,7 +1106,6 @@ App::get('/v1/users/identities')
if (!empty($search)) {
$queries[] = Query::search('search', $search);
}
/**
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
*/
@@ -1102,19 +1115,15 @@ App::get('/v1/users/identities')
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$identityId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('identities', $identityId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "User '{$identityId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
@@ -1354,12 +1363,17 @@ App::patch('/v1/users/:userId/password')
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
// Create Argon2 hasher with default settings
$hasher = new Argon2();
$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, $user->getAttribute('hash'), $user->getAttribute('hashOptions'));
$validator = new PasswordHistory($history, $hash);
if (!$validator->isValid($password)) {
throw new Exception(Exception::USER_PASSWORD_RECENTLY_USED);
}
@@ -1372,8 +1386,8 @@ App::patch('/v1/users/:userId/password')
->setAttribute('password', $newPassword)
->setAttribute('passwordHistory', $history)
->setAttribute('passwordUpdate', DateTime::now())
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS);
->setAttribute('hash', $hasher->getName())
->setAttribute('hashOptions', $hasher->getOptions());
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
@@ -2197,17 +2211,19 @@ App::post('/v1/users/:userId/sessions')
->inject('locale')
->inject('geodb')
->inject('queueForEvents')
->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $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) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION);
$secret = $proofForToken->generate();
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$session = new Document(array_merge(
@@ -2215,8 +2231,8 @@ App::post('/v1/users/:userId/sessions')
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getSequence(),
'provider' => Auth::SESSION_PROVIDER_SERVER,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'provider' => SESSION_PROVIDER_SERVER,
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'factors' => ['server'],
'ip' => $request->getIP(),
@@ -2240,8 +2256,13 @@ App::post('/v1/users/:userId/sessions')
$dbForProject->purgeCachedDocument('users', $user->getId());
$encoded = $store
->setProperty('id', $user->getId())
->setProperty('secret', $secret)
->encode();
$session
->setAttribute('secret', Auth::encodeSession($user->getId(), $secret))
->setAttribute('secret', $encoded)
->setAttribute('countryName', $countryName);
$queueForEvents
@@ -2276,7 +2297,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', Auth::TOKEN_EXPIRATION_GENERIC, new Range(60, Auth::TOKEN_EXPIRATION_LOGIN_LONG), 'Token expiration period in seconds. The default expiration is 15 minutes.', 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)
->inject('request')
->inject('response')
->inject('dbForProject')
@@ -2288,15 +2309,17 @@ App::post('/v1/users/:userId/tokens')
throw new Exception(Exception::USER_NOT_FOUND);
}
$secret = Auth::tokenGenerator($length);
$proofForToken = new Token($length);
$proofForToken->setHash(new Sha());
$secret = $proofForToken->generate();
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $expire));
$token = new Document([
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getSequence(),
'type' => Auth::TOKEN_TYPE_GENERIC,
'secret' => Auth::hash($secret),
'type' => TOKEN_TYPE_GENERIC,
'secret' => $proofForToken->hash($secret),
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP()
@@ -2609,7 +2632,8 @@ App::post('/v1/users/:userId/jwts')
$session = \count($sessions) > 0 ? $sessions[\count($sessions) - 1] : new Document();
} else {
// Find by ID
foreach ($sessions as $loopSession) { /** @var Utopia\Database\Document $loopSession */
foreach ($sessions as $loopSession) {
/** @var Utopia\Database\Document $loopSession */
if ($loopSession->getId() == $sessionId) {
$session = $loopSession;
break;
+101 -6
View File
@@ -14,9 +14,12 @@ use Appwrite\Utopia\Database\Validator\Queries\Installations;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Appwrite\Vcs\Comment;
use Swoole\Coroutine\WaitGroup;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Adapters\Dotenv as ConfigDotenv;
use Utopia\Config\Config;
use Utopia\Config\Exceptions\Parse;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
@@ -28,7 +31,10 @@ use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Detector\Detection\Framework\Analog;
use Utopia\Detector\Detection\Framework\Angular;
use Utopia\Detector\Detection\Framework\Astro;
@@ -963,6 +969,46 @@ App::post('/v1/vcs/github/installations/:installationId/detections')
throw new Exception(Exception::FUNCTION_RUNTIME_NOT_DETECTED);
}
}
$wg = new WaitGroup();
$envs = [];
foreach ($files as $file) {
if (!(\str_starts_with($file, '.env'))) {
continue;
}
$wg->add();
go(function () use ($github, $owner, $repositoryName, $providerRootDirectory, $file, $wg, &$envs) {
try {
$contentResponse = $github->getRepositoryContent($owner, $repositoryName, \rtrim($providerRootDirectory, '/') . '/' . $file);
$envFile = $contentResponse['content'] ?? '';
$configAdapter = new ConfigDotenv();
try {
$envObject = $configAdapter->parse($envFile);
foreach ($envObject as $envName => $envValue) {
$envs[$envName] = $envValue;
}
} catch (Parse $err) {
// Silence error, so rest of endpoint can return
}
} finally {
$wg->done();
}
});
}
$wg->wait();
$variables = [];
foreach ($envs as $key => $value) {
$variables[] = [
'name' => $key,
'value' => $value,
];
}
$output->setAttribute('variables', $variables);
$response->dynamic($output, $type === 'framework' ? Response::MODEL_DETECTION_FRAMEWORK : Response::MODEL_DETECTION_RUNTIME);
});
@@ -990,10 +1036,11 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
->param('installationId', '', new Text(256), 'Installation Id')
->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('gitHub')
->inject('response')
->inject('dbForPlatform')
->action(function (string $installationId, string $type, string $search, GitHub $github, Response $response, Database $dbForPlatform) {
->action(function (string $installationId, string $type, string $search, array $queries, GitHub $github, Response $response, Database $dbForPlatform) {
if (empty($search)) {
$search = "";
}
@@ -1009,11 +1056,20 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$page = 1;
$perPage = 4;
$queries = Query::parseQueries($queries);
$limitQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_LIMIT));
$offsetQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_OFFSET));
$limit = !empty($limitQuery) ? $limitQuery->getValue() : 4;
$offset = !empty($offsetQuery) ? $offsetQuery->getValue() : 0;
if ($offset % $limit !== 0) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'offset must be a multiple of the limit');
}
$page = ($offset / $limit) + 1;
$owner = $github->getOwnerName($providerInstallationId);
$repos = $github->searchRepositories($owner, $page, $perPage, $search);
['items' => $repos, 'total' => $total] = $github->searchRepositories($owner, $page, $limit, $search);
$repos = \array_map(function ($repo) use ($installation) {
$repo['id'] = \strval($repo['id'] ?? '');
@@ -1137,6 +1193,44 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
$repo['runtime'] = $runtimeWithVersion ?? '';
}
}
$wg = new WaitGroup();
$envs = [];
foreach ($files as $file) {
if (!(\str_starts_with($file, '.env'))) {
continue;
}
$wg->add();
go(function () use ($github, $repo, $file, $wg, &$envs) {
try {
$contentResponse = $github->getRepositoryContent($repo['organization'], $repo['name'], $file);
$envFile = $contentResponse['content'] ?? '';
$configAdapter = new ConfigDotenv();
try {
$envObject = $configAdapter->parse($envFile);
foreach ($envObject as $envName => $envValue) {
$envs[$envName] = $envValue;
}
} catch (Parse) {
// Silence error, so rest of endpoint can return
}
} finally {
$wg->done();
}
});
}
$wg->wait();
$repo['variables'] = [];
foreach ($envs as $key => $value) {
$repo['variables'][] = [
'name' => $key,
'value' => $value,
];
}
return $repo;
};
}, $repos));
@@ -1147,7 +1241,7 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
$response->dynamic(new Document([
$type === 'framework' ? 'frameworkProviderRepositories' : 'runtimeProviderRepositories' => $repos,
'total' => \count($repos),
'total' => $total,
]), ($type === 'framework') ? Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST : Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST);
});
@@ -1702,7 +1796,8 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$repository = Authorization::skip(fn () => $dbForPlatform->getDocument('repositories', $repositoryId, [
$repository = Authorization::skip(fn () => $dbForPlatform->findOne('repositories', [
Query::equal('$id', [$repositoryId]),
Query::equal('projectInternalId', [$project->getSequence()])
]));
+4 -4
View File
@@ -4,7 +4,6 @@ require_once __DIR__ . '/../init.php';
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Key;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
@@ -17,6 +16,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Transformation\Adapter\Preview;
use Appwrite\Transformation\Transformation;
use Appwrite\Utopia\Database\Documents\User as DBUser;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Request\Filters\V16 as RequestV16;
use Appwrite\Utopia\Request\Filters\V17 as RequestV17;
@@ -223,7 +223,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
*/
$requirePreview = \is_null($apiKey) || !$apiKey->isPreviewAuthDisabled();
if ($isPreview && $requirePreview) {
$cookie = $request->getCookie(Auth::$cookieNamePreview, '');
$cookie = $request->getCookie(COOKIE_NAME_PREVIEW, '');
$authorized = false;
// Security checks to mark authorized true
@@ -1260,7 +1260,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 (!DBUser::isPrivileged(Authorization::getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
@@ -1617,7 +1617,7 @@ App::get('/_appwrite/authorize')
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$response
->addCookie(Auth::$cookieNamePreview, $jwt, (new \DateTime($expire))->getTimestamp(), '/', $host, ('https' === $protocol), true, null)
->addCookie(COOKIE_NAME_PREVIEW, $jwt, (new \DateTime($expire))->getTimestamp(), '/', $host, ('https' === $protocol), true, null)
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($protocol . '://' . $host . $path);
+89 -29
View File
@@ -1,6 +1,5 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Auth\Key;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Event\Audit;
@@ -16,13 +15,13 @@ use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\SDK\Method;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Abuse\Abuse;
use Utopia\App;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@@ -34,7 +33,7 @@ use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Validator\WhiteList;
$parseLabel = function (string $label, array $responsePayload, array $requestParams, Document $user) {
$parseLabel = function (string $label, array $responsePayload, array $requestParams, User $user) {
preg_match_all('/{(.*?)}/', $label, $matches);
foreach ($matches[1] ?? [] as $pos => $match) {
$find = $matches[0][$pos];
@@ -232,44 +231,97 @@ 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) {
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey) {
$route = $utopia->getRoute();
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');
}
/**
* 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 ($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'];
// API Key authentication
// Step 5: API Key Authentication
if (!empty($apiKey)) {
// Verify no user session exists simultaneously
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
}
// Check if key is expired
if ($apiKey->isExpired()) {
throw new Exception(Exception::PROJECT_KEY_EXPIRED);
}
// Set role and scopes from API key
$role = $apiKey->getRole();
$scopes = $apiKey->getScopes();
if ($apiKey->getRole() === Auth::USER_ROLE_APPS) {
// Handle special app role case
if ($apiKey->getRole() === User::ROLE_APPS) {
// Disable authorization checks for API keys
Authorization::setDefaultStatus(false);
$user = new Document([
$user = new User([
'$id' => '',
'status' => true,
'type' => Auth::ACTIVITY_TYPE_APP,
'type' => ACTIVITY_TYPE_APP,
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => $apiKey->getName(),
@@ -278,6 +330,7 @@ App::init()
$queueForAudits->setUser($user);
}
// For standard keys, update last accessed time
if ($apiKey->getType() === API_KEY_STANDARD) {
$dbKey = $project->find(
key: 'secret',
@@ -343,11 +396,11 @@ App::init()
$scopes = \array_unique($scopes);
Authorization::setRole($role);
foreach (Auth::getRoles($user) as $authRole) {
foreach ($user->getRoles() as $authRole) {
Authorization::setRole($authRole);
}
// Update project last activity
// Step 6: Update project and user last activity
if (!$project->isEmpty() && $project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
@@ -356,7 +409,6 @@ App::init()
}
}
// Update user last activity
if (!empty($user->getId())) {
$accessedAt = $user->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
@@ -370,6 +422,7 @@ App::init()
}
}
// Steps 7-9: Access Control - Method, Namespace and Scope Validation
/**
* @var ?Method $method
*/
@@ -387,27 +440,29 @@ App::init()
if (
array_key_exists($namespace, $project->getAttribute('services', []))
&& !$project->getAttribute('services', [])[$namespace]
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
) {
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
}
}
// Do now allow access if scope is not allowed
// Step 9: Validate scope permissions
$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) . ')');
}
// Do not allow access to blocked accounts
// Step 10: Check if user is blocked
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);
@@ -415,6 +470,7 @@ 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);
@@ -454,7 +510,7 @@ App::init()
if (
array_key_exists('rest', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['rest']
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
@@ -485,8 +541,8 @@ App::init()
$closestLimit = null;
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
$isPrivilegedUser = User::isPrivileged($roles);
$isAppUser = User::isApp($roles);
foreach ($timeLimitArray as $timeLimit) {
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
@@ -541,7 +597,7 @@ App::init()
if (!$user->isEmpty()) {
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
$userClone->setAttribute('type', Auth::ACTIVITY_TYPE_USER);
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
$queueForAudits->setUser($userClone);
}
@@ -584,7 +640,7 @@ 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 && !User::isPrivileged(Authorization::getRoles());
$key = $request->cacheIdentifier();
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
@@ -607,12 +663,16 @@ App::init()
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAppUser && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
if (!$bucket->getAttribute('transformations', true) && !$isAppUser && !$isPrivilegedUser) {
throw new Exception(Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED);
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_READ);
$valid = $validator->isValid($bucket->getRead());
@@ -637,7 +697,7 @@ 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 (!User::isPrivileged(Authorization::getRoles())) {
$transformedAt = $file->getAttribute('transformedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
$file->setAttribute('transformedAt', DateTime::now());
@@ -737,7 +797,7 @@ 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) {
->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -785,7 +845,7 @@ App::shutdown()
if (!$user->isEmpty()) {
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
$userClone->setAttribute('type', Auth::ACTIVITY_TYPE_USER);
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
$queueForAudits->setUser($userClone);
} elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) {
/**
@@ -796,10 +856,10 @@ App::shutdown()
*
* Therefore, we consider this an anonymous request and create a relevant user.
*/
$user = new Document([
$user = new User([
'$id' => '',
'status' => true,
'type' => Auth::ACTIVITY_TYPE_GUEST,
'type' => ACTIVITY_TYPE_GUEST,
'email' => 'guest.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => 'Guest',
@@ -889,7 +949,7 @@ App::shutdown()
}
if ($project->getId() !== 'console') {
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
if (!User::isPrivileged(Authorization::getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
+4 -4
View File
@@ -1,7 +1,7 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use MaxMind\Db\Reader;
use Utopia\App;
@@ -20,7 +20,7 @@ App::init()
$lastUpdate = $session->getAttribute('mfaUpdatedAt');
if (!empty($lastUpdate)) {
$now = DateTime::now();
$maxAllowedDate = DateTime::addSeconds(new \DateTime($lastUpdate), Auth::MFA_RECENT_DURATION); // Maximum date until session is considered safe before asking for another challenge
$maxAllowedDate = DateTime::addSeconds(new \DateTime($lastUpdate), MFA_RECENT_DURATION); // Maximum date until session is considered safe before asking for another challenge
$isSessionFresh = DateTime::formatTz($maxAllowedDate) >= DateTime::formatTz($now);
}
@@ -49,8 +49,8 @@ App::init()
$route = $utopia->match($request);
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$isAppUser = User::isApp(Authorization::getRoles());
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
return;
+39 -36
View File
@@ -1,42 +1,45 @@
<?php
use Utopia\Config\Adapters\PHP;
use Utopia\Config\Config;
require_once __DIR__ . '/../config/storage/resource_limits.php';
Config::load('template-runtimes', __DIR__ . '/../config/template-runtimes.php');
Config::load('events', __DIR__ . '/../config/events.php');
Config::load('auth', __DIR__ . '/../config/auth.php');
Config::load('apis', __DIR__ . '/../config/apis.php'); // List of APIs
Config::load('errors', __DIR__ . '/../config/errors.php');
Config::load('oAuthProviders', __DIR__ . '/../config/oAuthProviders.php');
Config::load('platforms', __DIR__ . '/../config/platforms.php');
Config::load('console', __DIR__ . '/../config/console.php');
Config::load('collections', __DIR__ . '/../config/collections.php');
Config::load('frameworks', __DIR__ . '/../config/frameworks.php');
Config::load('runtimes', __DIR__ . '/../config/runtimes.php');
Config::load('runtimes-v2', __DIR__ . '/../config/runtimes-v2.php');
Config::load('usage', __DIR__ . '/../config/usage.php');
Config::load('roles', __DIR__ . '/../config/roles.php'); // User roles and scopes
Config::load('scopes', __DIR__ . '/../config/scopes.php'); // User roles and scopes
Config::load('services', __DIR__ . '/../config/services.php'); // List of services
Config::load('variables', __DIR__ . '/../config/variables.php'); // List of env variables
Config::load('regions', __DIR__ . '/../config/regions.php'); // List of available regions
Config::load('avatar-browsers', __DIR__ . '/../config/avatars/browsers.php');
Config::load('avatar-credit-cards', __DIR__ . '/../config/avatars/credit-cards.php');
Config::load('avatar-flags', __DIR__ . '/../config/avatars/flags.php');
Config::load('locale-codes', __DIR__ . '/../config/locale/codes.php');
Config::load('locale-currencies', __DIR__ . '/../config/locale/currencies.php');
Config::load('locale-eu', __DIR__ . '/../config/locale/eu.php');
Config::load('locale-languages', __DIR__ . '/../config/locale/languages.php');
Config::load('locale-phones', __DIR__ . '/../config/locale/phones.php');
Config::load('locale-countries', __DIR__ . '/../config/locale/countries.php');
Config::load('locale-continents', __DIR__ . '/../config/locale/continents.php');
Config::load('locale-templates', __DIR__ . '/../config/locale/templates.php');
Config::load('storage-logos', __DIR__ . '/../config/storage/logos.php');
Config::load('storage-mimes', __DIR__ . '/../config/storage/mimes.php');
Config::load('storage-inputs', __DIR__ . '/../config/storage/inputs.php');
Config::load('storage-outputs', __DIR__ . '/../config/storage/outputs.php');
Config::load('specifications', __DIR__ . '/../config/specifications.php');
Config::load('templates-function', __DIR__ . '/../config/templates/function.php');
Config::load('templates-site', __DIR__ . '/../config/templates/site.php');
$configAdapter = new PHP();
Config::load('template-runtimes', __DIR__ . '/../config/template-runtimes.php', $configAdapter);
Config::load('events', __DIR__ . '/../config/events.php', $configAdapter);
Config::load('auth', __DIR__ . '/../config/auth.php', $configAdapter);
Config::load('apis', __DIR__ . '/../config/apis.php', $configAdapter); // List of APIs
Config::load('errors', __DIR__ . '/../config/errors.php', $configAdapter);
Config::load('oAuthProviders', __DIR__ . '/../config/oAuthProviders.php', $configAdapter);
Config::load('platforms', __DIR__ . '/../config/platforms.php', $configAdapter);
Config::load('console', __DIR__ . '/../config/console.php', $configAdapter);
Config::load('collections', __DIR__ . '/../config/collections.php', $configAdapter);
Config::load('frameworks', __DIR__ . '/../config/frameworks.php', $configAdapter);
Config::load('runtimes', __DIR__ . '/../config/runtimes.php', $configAdapter);
Config::load('runtimes-v2', __DIR__ . '/../config/runtimes-v2.php', $configAdapter);
Config::load('usage', __DIR__ . '/../config/usage.php', $configAdapter);
Config::load('roles', __DIR__ . '/../config/roles.php', $configAdapter); // User roles and scopes
Config::load('scopes', __DIR__ . '/../config/scopes.php', $configAdapter); // User roles and scopes
Config::load('services', __DIR__ . '/../config/services.php', $configAdapter); // List of services
Config::load('variables', __DIR__ . '/../config/variables.php', $configAdapter); // List of env variables
Config::load('regions', __DIR__ . '/../config/regions.php', $configAdapter); // List of available regions
Config::load('avatar-browsers', __DIR__ . '/../config/avatars/browsers.php', $configAdapter);
Config::load('avatar-credit-cards', __DIR__ . '/../config/avatars/credit-cards.php', $configAdapter);
Config::load('avatar-flags', __DIR__ . '/../config/avatars/flags.php', $configAdapter);
Config::load('locale-codes', __DIR__ . '/../config/locale/codes.php', $configAdapter);
Config::load('locale-currencies', __DIR__ . '/../config/locale/currencies.php', $configAdapter);
Config::load('locale-eu', __DIR__ . '/../config/locale/eu.php', $configAdapter);
Config::load('locale-languages', __DIR__ . '/../config/locale/languages.php', $configAdapter);
Config::load('locale-phones', __DIR__ . '/../config/locale/phones.php', $configAdapter);
Config::load('locale-countries', __DIR__ . '/../config/locale/countries.php', $configAdapter);
Config::load('locale-continents', __DIR__ . '/../config/locale/continents.php', $configAdapter);
Config::load('locale-templates', __DIR__ . '/../config/locale/templates.php', $configAdapter);
Config::load('storage-logos', __DIR__ . '/../config/storage/logos.php', $configAdapter);
Config::load('storage-mimes', __DIR__ . '/../config/storage/mimes.php', $configAdapter);
Config::load('storage-inputs', __DIR__ . '/../config/storage/inputs.php', $configAdapter);
Config::load('storage-outputs', __DIR__ . '/../config/storage/outputs.php', $configAdapter);
Config::load('specifications', __DIR__ . '/../config/specifications.php', $configAdapter);
Config::load('templates-function', __DIR__ . '/../config/templates/function.php', $configAdapter);
Config::load('templates-site', __DIR__ . '/../config/templates/site.php', $configAdapter);
+66
View File
@@ -93,6 +93,69 @@ 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';
/**
* JWT for Resource Tokens.
*/
const RESOURCE_TOKEN_ALGORITHM = 'HS256';
const RESOURCE_TOKEN_MAX_AGE = 86400 * 365 * 10; /* 10 years */
const RESOURCE_TOKEN_LEEWAY = 10; // 10 seconds
/**
* 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;
@@ -297,3 +360,6 @@ const TOKENS_RESOURCE_TYPE_DATABASES = 'databases';
const SCHEDULE_RESOURCE_TYPE_EXECUTION = 'execution';
const SCHEDULE_RESOURCE_TYPE_FUNCTION = 'function';
const SCHEDULE_RESOURCE_TYPE_MESSAGE = 'message';
/** Preview cookie */
const COOKIE_NAME_PREVIEW = 'a_jwt_console';
+47 -1
View File
@@ -39,6 +39,7 @@ if (!App::isProduction()) {
PublicDomain::allow(['request-catcher-sms']);
PublicDomain::allow(['request-catcher-webhook']);
}
$register->set('logger', function () {
// Register error logger
$providerName = System::getEnv('_APP_LOGGING_PROVIDER', '');
@@ -97,6 +98,51 @@ $register->set('logger', function () {
return new Logger($adapter);
});
$register->set('realtimeLogger', function () {
// Register error logger for realtime, falls back to default logging config
$providerConfig = System::getEnv('_APP_LOGGING_CONFIG_REALTIME', '')
?: System::getEnv('_APP_LOGGING_CONFIG', '');
if (empty($providerConfig)) {
return;
}
$loggingProvider = new DSN($providerConfig);
$providerName = $loggingProvider->getScheme();
$providerConfig = match ($providerName) {
'sentry' => ['key' => $loggingProvider->getPassword(), 'projectId' => $loggingProvider->getUser() ?? '', 'host' => 'https://' . $loggingProvider->getHost()],
'logowl' => ['ticket' => $loggingProvider->getUser() ?? '', 'host' => $loggingProvider->getHost()],
default => ['key' => $loggingProvider->getHost()],
};
if (empty($providerName) || empty($providerConfig)) {
return;
}
if (!Logger::hasProvider($providerName)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Logging provider not supported. Logging is disabled");
}
try {
$adapter = match ($providerName) {
'sentry' => new Sentry($providerConfig['projectId'], $providerConfig['key'], $providerConfig['host']),
'logowl' => new LogOwl($providerConfig['ticket'], $providerConfig['host']),
'raygun' => new Raygun($providerConfig['key']),
'appsignal' => new AppSignal($providerConfig['key']),
default => null
};
} catch (Throwable $th) {
$adapter = null;
}
if ($adapter === null) {
Console::error("Logging provider not supported. Logging is disabled");
return;
}
return new Logger($adapter);
});
$register->set('pools', function () {
$group = new Group();
@@ -326,7 +372,7 @@ $register->set('smtp', function () {
return $mail;
});
$register->set('geodb', function () {
return new Reader(__DIR__ . '/../assets/dbip/dbip-country-lite-2024-09.mmdb');
return new Reader(__DIR__ . '/../assets/dbip/dbip-country-lite-2025-12.mmdb');
});
$register->set('passwordsDictionary', function () {
$content = \file_get_contents(__DIR__ . '/../assets/security/10k-common-passwords');
+99 -48
View File
@@ -2,7 +2,6 @@
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Key;
use Appwrite\Databases\TransactionState;
use Appwrite\Event\Audit;
@@ -23,10 +22,18 @@ use Appwrite\Extend\Exception;
use Appwrite\GraphQL\Schema;
use Appwrite\Network\Platform;
use Appwrite\Network\Validator\Origin;
use Appwrite\Utopia\Database\Documents\User;
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;
@@ -226,76 +233,91 @@ App::setResource('platforms', function (Request $request, Document $console, Doc
];
}, ['request', 'console', 'project', 'dbForPlatform']);
App::setResource('user', function ($mode, $project, $console, $request, $response, $dbForProject, $dbForPlatform) {
/** @var Appwrite\Utopia\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Database\Database $dbForPlatform */
/** @var string $mode */
App::setResource('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken) {
/**
* 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);
Auth::setCookieName('a_session_' . $project->getId());
$store->setKey('a_session_' . $project->getId());
if (APP_MODE_ADMIN === $mode) {
Auth::setCookieName('a_session_' . $console->getId());
$store->setKey('a_session_' . $console->getId());
}
$session = Auth::decodeSession(
$store->decode(
$request->getCookie(
Auth::$cookieName, // Get sessions
$request->getCookie(Auth::$cookieName . '_legacy', '')
$store->getKey(), // Get sessions
$request->getCookie($store->getKey() . '_legacy', '')
)
);
// Get session from header for SSR clients
if (empty($session['id']) && empty($session['secret'])) {
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
$sessionHeader = $request->getHeader('x-appwrite-session', '');
if (!empty($sessionHeader)) {
$session = Auth::decodeSession($sessionHeader);
$store->decode($sessionHeader);
}
}
// Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies
if ($response) {
if ($response) { // if in http context - add debug header
$response->addHeader('X-Debug-Fallback', 'false');
}
if (empty($session['id']) && empty($session['secret'])) {
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
if ($response) {
$response->addHeader('X-Debug-Fallback', 'true');
}
$fallback = $request->getHeader('x-fallback-cookies', '');
$fallback = \json_decode($fallback, true);
$session = Auth::decodeSession(((isset($fallback[Auth::$cookieName])) ? $fallback[Auth::$cookieName] : ''));
$store->decode(((is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : ''));
}
Auth::$unique = $session['id'] ?? '';
Auth::$secret = $session['secret'] ?? '';
$user = new Document([]);
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);
$user = null;
if (APP_MODE_ADMIN === $mode) {
/** @var User $user */
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
} else {
if ($project->isEmpty()) {
$user = new User([]);
} else {
if (!empty($store->getProperty('id', ''))) {
if ($project->getId() === 'console') {
/** @var User $user */
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
} else {
/** @var User $user */
$user = $dbForProject->getDocument('users', $store->getProperty('id', ''));
}
}
}
}
if (
!$user ||
$user->isEmpty() // Check a document has been found in the DB
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret)
|| !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken)
) { // Validate user has valid login token
$user = new Document([]);
$user = new User([]);
}
// if (APP_MODE_ADMIN === $mode) {
// if ($user->find('teamInternalId', $project->getAttribute('teamInternalId'), 'memberships')) {
// Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users.
@@ -303,18 +325,14 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
// $user = new Document([]);
// }
// }
$authJWT = $request->getHeader('x-appwrite-jwt', '');
if (!empty($authJWT) && !$project->isEmpty()) { // JWT authentication
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
try {
$payload = $jwt->decode($authJWT);
} catch (JWTException $error) {
throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage());
}
$jwtUserId = $payload['userId'] ?? '';
if (!empty($jwtUserId)) {
if ($mode === APP_MODE_ADMIN) {
@@ -323,20 +341,18 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
$user = $dbForProject->getDocument('users', $jwtUserId);
}
}
$jwtSessionId = $payload['sessionId'] ?? '';
if (!empty($jwtSessionId)) {
if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token
$user = new Document([]);
$user = new User([]);
}
}
}
$dbForProject->setMetadata('user', $user->getId());
$dbForPlatform->setMetadata('user', $user->getId());
return $user;
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform']);
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken']);
App::setResource('project', function ($dbForPlatform, $request, $console) {
/** @var Appwrite\Utopia\Request $request */
@@ -354,31 +370,61 @@ App::setResource('project', function ($dbForPlatform, $request, $console) {
return $project;
}, ['dbForPlatform', 'request', 'console']);
App::setResource('session', function (Document $user) {
App::setResource('session', function (User $user, Store $store, Token $proofForToken) {
if ($user->isEmpty()) {
return;
}
$sessions = $user->getAttribute('sessions', []);
$sessionId = Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret);
$sessionId = $user->sessionVerify($store->getProperty('secret', ''), $proofForToken);
if (!$sessionId) {
return;
}
foreach ($sessions as $session) {/** @var Document $session */
foreach ($sessions as $session) {
/** @var Document $session */
if ($sessionId === $session->getId()) {
return $session;
}
}
return;
}, ['user']);
}, ['user', 'store', 'proofForToken']);
App::setResource('console', function () {
return new Document(Config::getParam('console'));
}, []);
App::setResource('store', function (): Store {
return new Store();
});
App::setResource('proofForPassword', function (): Password {
$hash = new Argon2();
$hash
->setMemoryCost(7168)
->setTimeCost(5)
->setThreads(1);
$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('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
@@ -399,6 +445,7 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform
->setMetadata('project', $project->getId())
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
$database->setDocumentType('users', User::class);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
@@ -428,6 +475,8 @@ App::setResource('dbForPlatform', function (Group $pools, Cache $cache) {
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
$database->setDocumentType('users', User::class);
return $database;
}, ['pools', 'cache']);
@@ -452,6 +501,7 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
->setMetadata('project', $project->getId())
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
$database->setDocumentType('users', User::class);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
@@ -953,7 +1003,8 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
$tokenJWT = $request->getParam('token');
if (!empty($tokenJWT) && !$project->isEmpty()) { // JWT authentication
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway.
// Use a large but reasonable maxAge to avoid auto-exp when token has no expiry
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), RESOURCE_TOKEN_ALGORITHM, RESOURCE_TOKEN_MAX_AGE, RESOURCE_TOKEN_LEEWAY); // Instantiate with key, algo, maxAge and leeway.
try {
$payload = $jwt->decode($tokenJWT);
+94 -66
View File
@@ -1,13 +1,14 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Network\Validator\Origin;
use Appwrite\PubSub\Adapter\Pool as PubSubPool;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Swoole\Coroutine;
use Swoole\Http\Request as SwooleRequest;
use Swoole\Http\Response as SwooleResponse;
use Swoole\Runtime;
@@ -16,6 +17,9 @@ 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;
@@ -49,14 +53,14 @@ Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
if (!function_exists('getConsoleDB')) {
function getConsoleDB(): Database
{
global $register;
$ctx = Coroutine::getContext();
static $database = null;
if ($database !== null) {
return $database;
if (isset($ctx['dbForPlatform'])) {
return $ctx['dbForPlatform'];
}
global $register;
/** @var Group $pools */
$pools = $register->get('pools');
@@ -66,8 +70,8 @@ if (!function_exists('getConsoleDB')) {
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', '_console');
return $database;
$database->setDocumentType('users', User::class);
return $ctx['dbForPlatform'] = $database;
}
}
@@ -75,14 +79,18 @@ if (!function_exists('getConsoleDB')) {
if (!function_exists('getProjectDB')) {
function getProjectDB(Document $project): Database
{
global $register;
$ctx = Coroutine::getContext();
static $databases = [];
if (isset($databases[$project->getSequence()])) {
return $databases[$project->getSequence()];
if (!isset($ctx['dbForProject'])) {
$ctx['dbForProject'] = [];
}
if (isset($ctx['dbForProject'][$project->getSequence()])) {
return $ctx['dbForProject'][$project->getSequence()];
}
global $register;
/** @var Group $pools */
$pools = $register->get('pools');
@@ -118,7 +126,9 @@ if (!function_exists('getProjectDB')) {
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId());
return $databases[$project->getSequence()] = $database;
$database->setDocumentType('users', User::class);
return $ctx['dbForProject'][$project->getSequence()] = $database;
}
}
@@ -126,14 +136,14 @@ if (!function_exists('getProjectDB')) {
if (!function_exists('getCache')) {
function getCache(): Cache
{
global $register;
$ctx = Coroutine::getContext();
static $cache = null;
if ($cache !== null) {
return $cache;
if (isset($ctx['cache'])) {
return $ctx['cache'];
}
global $register;
$pools = $register->get('pools'); /** @var Group $pools */
$list = Config::getParam('pools-cache', []);
@@ -143,7 +153,7 @@ if (!function_exists('getCache')) {
$adapters[] = new CachePool($pools->get($value));
}
return $cache = new Cache(new Sharding($adapters));
return $ctx['cache'] = new Cache(new Sharding($adapters));
}
}
@@ -151,10 +161,10 @@ if (!function_exists('getCache')) {
if (!function_exists('getRedis')) {
function getRedis(): \Redis
{
static $redis = null;
$ctx = Coroutine::getContext();
if ($redis !== null) {
return $redis;
if (isset($ctx['redis'])) {
return $ctx['redis'];
}
$host = System::getEnv('_APP_REDIS_HOST', 'localhost');
@@ -168,46 +178,46 @@ if (!function_exists('getRedis')) {
}
$redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
return $redis;
return $ctx['redis'] = $redis;
}
}
if (!function_exists('getTimelimit')) {
function getTimelimit(): TimeLimitRedis
function getTimelimit(string $key = "", int $limit = 0, int $seconds = 1): TimeLimitRedis
{
static $timelimit = null;
$ctx = Coroutine::getContext();
if ($timelimit !== null) {
return $timelimit;
if (isset($ctx['timelimit'])) {
return $ctx['timelimit'];
}
return $timelimit = new TimeLimitRedis("", 0, 1, getRedis());
return $ctx['timelimit'] = new TimeLimitRedis($key, $limit, $seconds, getRedis());
}
}
if (!function_exists('getRealtime')) {
function getRealtime(): Realtime
{
static $realtime = null;
$ctx = Coroutine::getContext();
if ($realtime !== null) {
return $realtime;
if (isset($ctx['realtime'])) {
return $ctx['realtime'];
}
return $realtime = new Realtime();
return $ctx['realtime'] = new Realtime();
}
}
if (!function_exists('getTelemetry')) {
function getTelemetry(int $workerId): Utopia\Telemetry\Adapter
{
static $telemetry = null;
$ctx = Coroutine::getContext();
if ($telemetry !== null) {
return $telemetry;
if (isset($ctx['telemetry'])) {
return $ctx['telemetry'];
}
return $telemetry = new NoTelemetry();
return $ctx['telemetry'] = new NoTelemetry();
}
}
@@ -236,7 +246,7 @@ $adapter
$server = new Server($adapter);
$logError = function (Throwable $error, string $action) use ($register) {
$logger = $register->get('logger');
$logger = $register->get('realtimeLogger');
if ($logger && !$error instanceof Exception) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
@@ -284,28 +294,35 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
* Create document for this worker to share stats across Containers.
*/
go(function () use ($register, $containerId, &$statsDocument) {
$attempts = 0;
$database = getConsoleDB();
$document = new Document([
'$id' => ID::unique(),
'$collection' => ID::custom('realtime'),
'$permissions' => [],
'container' => $containerId,
'timestamp' => DateTime::now(),
'value' => '{}'
]);
try {
$statsDocument = Authorization::skip(fn () => $database->createDocument('realtime', $document));
} catch (\Exception $e) {
$logger = $register->get('realtimeLogger');
if ($logger) {
$log = new Log();
$log->setNamespace("realtime");
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion(System::getEnv('_APP_VERSION', 'UNKNOWN'));
$log->setType(Log::TYPE_ERROR);
do {
try {
$attempts++;
$document = new Document([
'$id' => ID::unique(),
'$collection' => ID::custom('realtime'),
'$permissions' => [],
'container' => $containerId,
'timestamp' => DateTime::now(),
'value' => '{}'
]);
$log->setMessage($e->getMessage());
$statsDocument = Authorization::skip(fn () => $database->createDocument('realtime', $document));
break;
} catch (Throwable) {
Console::warning("Collection not ready. Retrying connection ({$attempts})...");
sleep(DATABASE_RECONNECT_SLEEP);
$log->addExtra('file', $e->getFile());
$log->addExtra('line', $e->getLine());
$log->addExtra('trace', $e->getTraceAsString());
$logger->addLog($log);
}
} while (true);
}
});
/**
@@ -457,9 +474,10 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$project = Authorization::skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
$database = getProjectDB($project);
/** @var Appwrite\Utopia\Database\Documents\User $user */
$user = $database->getDocument('users', $userId);
$roles = Auth::getRoles($user);
$roles = $user->getRoles();
$channels = $realtime->connections[$connection]['channels'];
$realtime->unsubscribe($connection);
@@ -526,14 +544,14 @@ $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()))
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
$timelimit = $app->getResource('timelimit');
$platforms = $app->getResource('platforms');
$user = $app->getResource('user'); /** @var Document $user */
$user = $app->getResource('user'); /** @var User $user */
/*
* Abuse Check
@@ -563,7 +581,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
}
$roles = Auth::getRoles($user);
$roles = $user->getRoles();
$channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId());
@@ -678,21 +696,31 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
}
$session = Auth::decodeSession($message['data']['session']);
Auth::$unique = $session['id'] ?? '';
Auth::$secret = $session['secret'] ?? '';
$store = new Store();
$user = $database->getDocument('users', Auth::$unique);
$store->decode($message['data']['session']);
/** @var User $user */
$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());
if (
empty($user->getId()) // Check a document has been found in the DB
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret) // Validate user has valid login token
|| !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken) // 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 = $user->getRoles();
$channels = Realtime::convertChannels(array_flip($realtime->connections[$connection]['channels']), $user->getId());
$realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels);
+3 -3
View File
@@ -849,7 +849,7 @@ $image = $this->getParam('image', '');
- _APP_DB_PASS
appwrite-assistant:
image: appwrite/assistant:0.8.3
image: appwrite/assistant:0.8.4
container_name: appwrite-assistant
<<: *x-logging
restart: unless-stopped
@@ -857,9 +857,9 @@ $image = $this->getParam('image', '');
- appwrite
environment:
- _APP_ASSISTANT_OPENAI_API_KEY
appwrite-browser:
image: appwrite/browser:0.3.1
image: appwrite/browser:0.3.2
container_name: appwrite-browser
<<: *x-logging
restart: unless-stopped
+3 -1
View File
@@ -17,6 +17,7 @@ use Appwrite\Event\Realtime;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Platform\Appwrite;
use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Swoole\Runtime;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
@@ -55,7 +56,7 @@ Server::setResource('dbForPlatform', function (Cache $cache, Registry $register)
$adapter = new DatabasePool($pools->get('console'));
$dbForPlatform = new Database($adapter, $cache);
$dbForPlatform->setNamespace('_console');
$dbForPlatform->setDocumentType('users', User::class);
return $dbForPlatform;
}, ['cache', 'register']);
@@ -86,6 +87,7 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$database->setDocumentType('users', User::class);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
+4 -3
View File
@@ -48,9 +48,10 @@
"utopia-php/abuse": "1.*",
"utopia-php/analytics": "0.10.*",
"utopia-php/audit": "1.*",
"utopia-php/auth": "0.5.*",
"utopia-php/cache": "0.13.*",
"utopia-php/cli": "0.15.*",
"utopia-php/config": "0.2.*",
"utopia-php/config": "1.*.*",
"utopia-php/database": "3.*",
"utopia-php/detector": "0.2.*",
"utopia-php/domains": "0.9.*",
@@ -63,7 +64,7 @@
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.20.*",
"utopia-php/migration": "1.*",
"utopia-php/migration": "1.3.*",
"utopia-php/orchestration": "0.9.*",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "0.8.*",
@@ -74,7 +75,7 @@
"utopia-php/swoole": "0.8.*",
"utopia-php/system": "0.9.*",
"utopia-php/telemetry": "0.1.*",
"utopia-php/vcs": "0.12.*",
"utopia-php/vcs": "0.13.*",
"utopia-php/websocket": "0.3.*",
"matomo/device-detector": "6.4.*",
"dragonmantank/cron-expression": "3.4.*",
Generated
+276 -231
View File
File diff suppressed because it is too large Load Diff
-7
View File
@@ -1,7 +0,0 @@
services:
appwrite:
# volumes:
# - ~/.ssh:/root/.ssh
environment:
- GH_TOKEN=
- GIT_EMAIL=
+3 -2
View File
@@ -285,6 +285,7 @@ services:
- _APP_DB_PASS
- _APP_USAGE_STATS
- _APP_LOGGING_CONFIG
- _APP_LOGGING_CONFIG_REALTIME
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-audits:
@@ -950,7 +951,7 @@ services:
appwrite-assistant:
container_name: appwrite-assistant
image: appwrite/assistant:0.8.3
image: appwrite/assistant:0.8.4
networks:
- appwrite
environment:
@@ -958,7 +959,7 @@ services:
appwrite-browser:
container_name: appwrite-browser
image: appwrite/browser:0.3.1
image: appwrite/browser:0.3.2
networks:
- appwrite
@@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
@@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: &lt;REGION&gt;.cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0
@@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
@@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
@@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: &lt;REGION&gt;.cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0
@@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
@@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: &lt;REGION&gt;.cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0
@@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
@@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
@@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: &lt;REGION&gt;.cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0
@@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0
@@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0
@@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0
@@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0
@@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.7.0
@@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.7.0
@@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.7.0
@@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.7.0
@@ -13,7 +13,7 @@ account.createOAuth2Session(
OAuthProvider.AMAZON, // provider
"https://example.com", // success (optional)
"https://example.com", // failure (optional)
listOf(), // scopes (optional)
List.of(), // scopes (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -13,7 +13,7 @@ account.createOAuth2Token(
OAuthProvider.AMAZON, // provider
"https://example.com", // success (optional)
"https://example.com", // failure (optional)
listOf(), // scopes (optional)
List.of(), // scopes (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -9,7 +9,7 @@ Client client = new Client(context)
Account account = new Account(client);
account.listIdentities(
listOf(), // queries (optional)
List.of(), // queries (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -9,7 +9,7 @@ Client client = new Client(context)
Account account = new Account(client);
account.listLogs(
listOf(), // queries (optional)
List.of(), // queries (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -9,10 +9,10 @@ Client client = new Client(context)
Account account = new Account(client);
account.updatePrefs(
mapOf(
"language" to "en",
"timezone" to "UTC",
"darkTheme" to true
Map.of(
"language", "en",
"timezone", "UTC",
"darkTheme", true
), // prefs
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -1,6 +1,9 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Avatars;
import io.appwrite.enums.Theme;
import io.appwrite.enums.Timezone;
import io.appwrite.enums.Output;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -10,25 +13,25 @@ Avatars avatars = new Avatars(client);
avatars.getScreenshot(
"https://example.com", // url
mapOf( "a" to "b" ), // headers (optional)
Map.of("a", "b"), // headers (optional)
1, // viewportWidth (optional)
1, // viewportHeight (optional)
0.1, // scale (optional)
theme.LIGHT, // theme (optional)
Theme.LIGHT, // theme (optional)
"<USER_AGENT>", // userAgent (optional)
false, // fullpage (optional)
"<LOCALE>", // locale (optional)
timezone.AFRICA_ABIDJAN, // timezone (optional)
Timezone.AFRICA_ABIDJAN, // timezone (optional)
-90, // latitude (optional)
-180, // longitude (optional)
0, // accuracy (optional)
false, // touch (optional)
listOf(), // permissions (optional)
List.of(), // permissions (optional)
0, // sleep (optional)
0, // width (optional)
0, // height (optional)
-1, // quality (optional)
output.JPG, // output (optional)
Output.JPG, // output (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -1,8 +1,8 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Databases;
import io.appwrite.Permission;
import io.appwrite.Role;
import io.appwrite.services.Databases;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -14,14 +14,14 @@ databases.createDocument(
"<DATABASE_ID>", // databaseId
"<COLLECTION_ID>", // collectionId
"<DOCUMENT_ID>", // documentId
mapOf(
"username" to "walter.obrien",
"email" to "walter.obrien@example.com",
"fullName" to "Walter O'Brien",
"age" to 30,
"isAdmin" to false
Map.of(
"username", "walter.obrien",
"email", "walter.obrien@example.com",
"fullName", "Walter O'Brien",
"age", 30,
"isAdmin", false
), // data
listOf(Permission.read(Role.any())), // permissions (optional)
List.of(Permission.read(Role.any())), // permissions (optional)
"<TRANSACTION_ID>", // transactionId (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -10,17 +10,15 @@ Databases databases = new Databases(client);
databases.createOperations(
"<TRANSACTION_ID>", // transactionId
listOf(
{
"action": "create",
"databaseId": "<DATABASE_ID>",
"collectionId": "<COLLECTION_ID>",
"documentId": "<DOCUMENT_ID>",
"data": {
"name": "Walter O'Brien"
}
}
), // operations (optional)
List.of(Map.of(
"action", "create",
"databaseId", "<DATABASE_ID>",
"collectionId", "<COLLECTION_ID>",
"documentId", "<DOCUMENT_ID>",
"data", Map.of(
"name", "Walter O'Brien"
)
)), // operations (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -12,7 +12,7 @@ databases.getDocument(
"<DATABASE_ID>", // databaseId
"<COLLECTION_ID>", // collectionId
"<DOCUMENT_ID>", // documentId
listOf(), // queries (optional)
List.of(), // queries (optional)
"<TRANSACTION_ID>", // transactionId (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -11,7 +11,7 @@ Databases databases = new Databases(client);
databases.listDocuments(
"<DATABASE_ID>", // databaseId
"<COLLECTION_ID>", // collectionId
listOf(), // queries (optional)
List.of(), // queries (optional)
"<TRANSACTION_ID>", // transactionId (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
@@ -9,7 +9,7 @@ Client client = new Client(context)
Databases databases = new Databases(client);
databases.listTransactions(
listOf(), // queries (optional)
List.of(), // queries (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -1,8 +1,8 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Databases;
import io.appwrite.Permission;
import io.appwrite.Role;
import io.appwrite.services.Databases;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -14,8 +14,8 @@ databases.updateDocument(
"<DATABASE_ID>", // databaseId
"<COLLECTION_ID>", // collectionId
"<DOCUMENT_ID>", // documentId
mapOf( "a" to "b" ), // data (optional)
listOf(Permission.read(Role.any())), // permissions (optional)
Map.of("a", "b"), // data (optional)
List.of(Permission.read(Role.any())), // permissions (optional)
"<TRANSACTION_ID>", // transactionId (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -1,8 +1,8 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Databases;
import io.appwrite.Permission;
import io.appwrite.Role;
import io.appwrite.services.Databases;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -14,8 +14,8 @@ databases.upsertDocument(
"<DATABASE_ID>", // databaseId
"<COLLECTION_ID>", // collectionId
"<DOCUMENT_ID>", // documentId
mapOf( "a" to "b" ), // data
listOf(Permission.read(Role.any())), // permissions (optional)
Map.of("a", "b"), // data
List.of(Permission.read(Role.any())), // permissions (optional)
"<TRANSACTION_ID>", // transactionId (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -1,6 +1,7 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Functions;
import io.appwrite.enums.ExecutionMethod;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -14,7 +15,7 @@ functions.createExecution(
false, // async (optional)
"<PATH>", // path (optional)
ExecutionMethod.GET, // method (optional)
mapOf( "a" to "b" ), // headers (optional)
Map.of("a", "b"), // headers (optional)
"<SCHEDULED_AT>", // scheduledAt (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -10,7 +10,7 @@ Functions functions = new Functions(client);
functions.listExecutions(
"<FUNCTION_ID>", // functionId
listOf(), // queries (optional)
List.of(), // queries (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -9,7 +9,7 @@ Client client = new Client(context)
Graphql graphql = new Graphql(client);
graphql.mutation(
mapOf( "a" to "b" ), // query
Map.of("a", "b"), // query
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -9,7 +9,7 @@ Client client = new Client(context)
Graphql graphql = new Graphql(client);
graphql.query(
mapOf( "a" to "b" ), // query
Map.of("a", "b"), // query
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -1,9 +1,9 @@
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;
import io.appwrite.services.Storage;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -15,7 +15,7 @@ storage.createFile(
"<BUCKET_ID>", // bucketId
"<FILE_ID>", // fileId
InputFile.fromPath("file.png"), // file
listOf(Permission.read(Role.any())), // permissions (optional)
List.of(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.Storage;
import io.appwrite.enums.ImageGravity;
import io.appwrite.enums.ImageFormat;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -10,7 +10,7 @@ Storage storage = new Storage(client);
storage.listFiles(
"<BUCKET_ID>", // bucketId
listOf(), // queries (optional)
List.of(), // queries (optional)
"<SEARCH>", // search (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
@@ -1,8 +1,8 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Storage;
import io.appwrite.Permission;
import io.appwrite.Role;
import io.appwrite.services.Storage;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -14,7 +14,7 @@ storage.updateFile(
"<BUCKET_ID>", // bucketId
"<FILE_ID>", // fileId
"<NAME>", // name (optional)
listOf(Permission.read(Role.any())), // permissions (optional)
List.of(Permission.read(Role.any())), // permissions (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -10,17 +10,15 @@ TablesDB tablesDB = new TablesDB(client);
tablesDB.createOperations(
"<TRANSACTION_ID>", // transactionId
listOf(
{
"action": "create",
"databaseId": "<DATABASE_ID>",
"tableId": "<TABLE_ID>",
"rowId": "<ROW_ID>",
"data": {
"name": "Walter O'Brien"
}
}
), // operations (optional)
List.of(Map.of(
"action", "create",
"databaseId", "<DATABASE_ID>",
"tableId", "<TABLE_ID>",
"rowId", "<ROW_ID>",
"data", Map.of(
"name", "Walter O'Brien"
)
)), // operations (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -1,8 +1,8 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.TablesDB;
import io.appwrite.Permission;
import io.appwrite.Role;
import io.appwrite.services.TablesDB;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -14,14 +14,14 @@ tablesDB.createRow(
"<DATABASE_ID>", // databaseId
"<TABLE_ID>", // tableId
"<ROW_ID>", // rowId
mapOf(
"username" to "walter.obrien",
"email" to "walter.obrien@example.com",
"fullName" to "Walter O'Brien",
"age" to 30,
"isAdmin" to false
Map.of(
"username", "walter.obrien",
"email", "walter.obrien@example.com",
"fullName", "Walter O'Brien",
"age", 30,
"isAdmin", false
), // data
listOf(Permission.read(Role.any())), // permissions (optional)
List.of(Permission.read(Role.any())), // permissions (optional)
"<TRANSACTION_ID>", // transactionId (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -12,7 +12,7 @@ tablesDB.getRow(
"<DATABASE_ID>", // databaseId
"<TABLE_ID>", // tableId
"<ROW_ID>", // rowId
listOf(), // queries (optional)
List.of(), // queries (optional)
"<TRANSACTION_ID>", // transactionId (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -11,7 +11,7 @@ TablesDB tablesDB = new TablesDB(client);
tablesDB.listRows(
"<DATABASE_ID>", // databaseId
"<TABLE_ID>", // tableId
listOf(), // queries (optional)
List.of(), // queries (optional)
"<TRANSACTION_ID>", // transactionId (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
@@ -9,7 +9,7 @@ Client client = new Client(context)
TablesDB tablesDB = new TablesDB(client);
tablesDB.listTransactions(
listOf(), // queries (optional)
List.of(), // queries (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -1,8 +1,8 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.TablesDB;
import io.appwrite.Permission;
import io.appwrite.Role;
import io.appwrite.services.TablesDB;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -14,8 +14,8 @@ tablesDB.updateRow(
"<DATABASE_ID>", // databaseId
"<TABLE_ID>", // tableId
"<ROW_ID>", // rowId
mapOf( "a" to "b" ), // data (optional)
listOf(Permission.read(Role.any())), // permissions (optional)
Map.of("a", "b"), // data (optional)
List.of(Permission.read(Role.any())), // permissions (optional)
"<TRANSACTION_ID>", // transactionId (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -1,8 +1,8 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.TablesDB;
import io.appwrite.Permission;
import io.appwrite.Role;
import io.appwrite.services.TablesDB;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -14,8 +14,8 @@ tablesDB.upsertRow(
"<DATABASE_ID>", // databaseId
"<TABLE_ID>", // tableId
"<ROW_ID>", // rowId
mapOf( "a" to "b" ), // data (optional)
listOf(Permission.read(Role.any())), // permissions (optional)
Map.of("a", "b"), // data (optional)
List.of(Permission.read(Role.any())), // permissions (optional)
"<TRANSACTION_ID>", // transactionId (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -10,7 +10,7 @@ Teams teams = new Teams(client);
teams.createMembership(
"<TEAM_ID>", // teamId
listOf(), // roles
List.of(), // roles
"email@example.com", // email (optional)
"<USER_ID>", // userId (optional)
"+12065550100", // phone (optional)
@@ -11,7 +11,7 @@ Teams teams = new Teams(client);
teams.create(
"<TEAM_ID>", // teamId
"<NAME>", // name
listOf(), // roles (optional)
List.of(), // roles (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -10,7 +10,7 @@ Teams teams = new Teams(client);
teams.listMemberships(
"<TEAM_ID>", // teamId
listOf(), // queries (optional)
List.of(), // queries (optional)
"<SEARCH>", // search (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
@@ -9,7 +9,7 @@ Client client = new Client(context)
Teams teams = new Teams(client);
teams.list(
listOf(), // queries (optional)
List.of(), // queries (optional)
"<SEARCH>", // search (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
@@ -11,7 +11,7 @@ Teams teams = new Teams(client);
teams.updateMembership(
"<TEAM_ID>", // teamId
"<MEMBERSHIP_ID>", // membershipId
listOf(), // roles
List.of(), // roles
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -10,7 +10,7 @@ Teams teams = new Teams(client);
teams.updatePrefs(
"<TEAM_ID>", // teamId
mapOf( "a" to "b" ), // prefs
Map.of("a", "b"), // prefs
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -1,6 +1,9 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.Avatars
import io.appwrite.enums.Theme
import io.appwrite.enums.Timezone
import io.appwrite.enums.Output
val client = Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -10,15 +10,13 @@ val databases = Databases(client)
val result = databases.createOperations(
transactionId = "<TRANSACTION_ID>",
operations = listOf(
{
"action": "create",
"databaseId": "<DATABASE_ID>",
"collectionId": "<COLLECTION_ID>",
"documentId": "<DOCUMENT_ID>",
"data": {
"name": "Walter O'Brien"
}
}
), // (optional)
operations = listOf(mapOf(
"action" to "create",
"databaseId" to "<DATABASE_ID>",
"collectionId" to "<COLLECTION_ID>",
"documentId" to "<DOCUMENT_ID>",
"data" to mapOf(
"name" to "Walter O'Brien"
)
)), // (optional)
)
@@ -1,6 +1,7 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.Functions
import io.appwrite.enums.ExecutionMethod
val client = Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -1,6 +1,8 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.Storage
import io.appwrite.enums.ImageGravity
import io.appwrite.enums.ImageFormat
val client = Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -10,15 +10,13 @@ val tablesDB = TablesDB(client)
val result = tablesDB.createOperations(
transactionId = "<TRANSACTION_ID>",
operations = listOf(
{
"action": "create",
"databaseId": "<DATABASE_ID>",
"tableId": "<TABLE_ID>",
"rowId": "<ROW_ID>",
"data": {
"name": "Walter O'Brien"
}
}
), // (optional)
operations = listOf(mapOf(
"action" to "create",
"databaseId" to "<DATABASE_ID>",
"tableId" to "<TABLE_ID>",
"rowId" to "<ROW_ID>",
"data" to mapOf(
"name" to "Walter O'Brien"
)
)), // (optional)
)
@@ -7,7 +7,7 @@ Client client = Client()
Avatars avatars = Avatars(client);
// Downloading file
UInt8List bytes = await avatars.getBrowser(
Uint8List bytes = await avatars.getBrowser(
code: Browser.avantBrowser,
width: 0, // optional
height: 0, // optional
@@ -7,7 +7,7 @@ Client client = Client()
Avatars avatars = Avatars(client);
// Downloading file
UInt8List bytes = await avatars.getCreditCard(
Uint8List bytes = await avatars.getCreditCard(
code: CreditCard.americanExpress,
width: 0, // optional
height: 0, // optional

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