Compare commits

...
Author SHA1 Message Date
Jake Barnby cdd2be175f Pre-parse queries 2026-02-04 19:27:57 +13:00
Jake Barnby 2f5e5d384f Array push on hot loop 2026-02-04 19:27:18 +13:00
Jake Barnby 2c790ecd84 Fix potential depth leak 2026-02-04 19:22:00 +13:00
Jake Barnby 5733972218 Fix webhook leak 2026-02-04 19:20:42 +13:00
Jake Barnby 2416eef4a7 Fix stats leak 2026-02-04 19:20:19 +13:00
Jake BarnbyandGitHub 17fc91aa19 Merge pull request #11238 from appwrite/fix-span-country-codes-logql
fix: use individual span attributes for country codes
2026-02-04 05:16:15 +00:00
Chirag Aggarwal b89f474cb5 fix skill 2026-02-04 10:20:31 +05:30
Chirag Aggarwal 345d8f2db5 fix: use individual span attributes for country codes
Replace stringified JSON countryCodes attribute with individual
countryCode_{code} attributes to improve LogQL query compatibility.
2026-02-04 10:15:36 +05:30
Chirag AggarwalandGitHub 5537fe210e Merge pull request #11236 from appwrite/safeguard-span-finish 2026-02-03 18:32:32 +05:30
Chirag Aggarwal 4c08cb36a8 chore: safeguard span finish 2026-02-03 18:30:51 +05:30
Chirag AggarwalandGitHub 1ee3f993d0 Merge pull request #11234 from appwrite/update-cli-changelog 2026-02-03 17:36:43 +05:30
Jake BarnbyandGitHub 952f209a44 Merge pull request #11202 from appwrite/realtime-query-subscriptions 2026-02-03 11:44:13 +00:00
Jake BarnbyandGitHub c04353bd38 Merge pull request #11230 from appwrite/fix-responses 2026-02-03 11:37:09 +00:00
Chirag Aggarwal 69cfde440a chore: fix cli release + add changelog 2026-02-03 17:00:22 +05:30
Chirag AggarwalandGitHub 739b36b0c9 Merge pull request #11233 from appwrite/wrap-markdown-docs 2026-02-03 16:17:18 +05:30
Jake Barnby d2eb3f6684 Update database 2026-02-03 23:34:13 +13:00
Chirag Aggarwal 31b5a1cb36 chore: wrap all markdown docs with language fences 2026-02-03 15:48:48 +05:30
Chirag AggarwalandGitHub 727c29ec0c Merge pull request #11232 from appwrite/chore-add-pr-workflow-skill 2026-02-03 15:29:20 +05:30
Chirag Aggarwal fab0c7ce7c Add PR workflow skill for AI agents
Documents that PRs should target version branches (e.g., 1.8.x)
instead of main, helping AI agents follow the correct workflow.
2026-02-03 15:12:47 +05:30
Jake Barnby 413a91bd0a Lint 2026-02-03 22:33:51 +13:00
Jake Barnby 23c3496a63 Add missing model map 2026-02-03 22:30:02 +13:00
ArnabChatterjee20k 0528c0d693 updated the case for select all and empty payload 2026-02-03 14:29:34 +05:30
ArnabChatterjee20k 4319612358 updated unit tests 2026-02-03 14:16:35 +05:30
Chirag AggarwalandGitHub b7df5cfcc1 Merge pull request #11228 from appwrite/feat-span-messaging-worker 2026-02-03 14:15:07 +05:30
ArnabChatterjee20k a10fbc64c7 updated subscription after permission change 2026-02-03 13:46:05 +05:30
Jake BarnbyandGitHub def3ce056f Merge pull request #11220 from appwrite/chore-upgrade-cache 2026-02-03 07:19:12 +00:00
Chirag Aggarwal ae720c3462 Fix country code extraction using locale config
Use the existing locale-phones config to accurately extract country
codes from phone numbers. Sorts codes by length descending to match
longer codes first (e.g., 1868 Trinidad before 1 USA).
2026-02-03 12:36:46 +05:30
Chirag Aggarwal e14a026415 Add country code extraction for external SMS messages
Extract country codes from target identifiers for external SMS
messages to match the behavior of internal SMS messages.
2026-02-03 12:32:21 +05:30
Chirag Aggarwal 52cb8a9c56 Improve span data with detailed metrics
Add richer span data for better observability:
- External messages: messageId, topic/user/target counts, recipient total,
  provider details, delivery stats (delivered/errors), final status
- Internal SMS: recipient count, country codes extracted from phone numbers,
  sender number, delivery status and count
2026-02-03 12:27:27 +05:30
Chirag Aggarwal 787e7aed43 Add Span tracing to Messaging worker
Replace Console logging with Span tracing in the Messaging worker
for better observability. Initialize Span storage and exporter in
worker.php to enable tracing in all workers.
2026-02-03 11:59:49 +05:30
ArnabChatterjee20k d18d4d8263 updated subscription process 2026-02-03 11:43:23 +05:30
premtsd-codeandGitHub b4e98bd950 Merge branch '1.8.x' into chore-upgrade-cache 2026-02-03 05:58:33 +00:00
Chirag AggarwalandGitHub 92d2310a53 Merge pull request #11227 from appwrite/fix-deprecation-warnings 2026-02-03 11:19:23 +05:30
ArnabChatterjee20kandGitHub 29b6adf407 Merge branch '1.8.x' into realtime-query-subscriptions 2026-02-03 10:46:34 +05:30
ArnabChatterjee20k 10e971aded updated tests and empty subscriptionsid issue 2026-02-03 10:46:01 +05:30
premtsd-codeandGitHub ca464f8a71 Merge branch '1.8.x' into chore-upgrade-cache 2026-02-03 04:44:12 +00:00
Chirag Aggarwal 6c4394947f Fix Safe PHP deprecation warnings 2026-02-03 10:11:12 +05:30
Chirag AggarwalandGitHub 2a15c91768 Merge pull request #11226 from appwrite/feat-functions-queue-job-ttl 2026-02-03 10:09:25 +05:30
Chirag Aggarwal f15e9eb356 Add 7-day job TTL for functions queue
Set jobTTL to 7 days for FUNCTIONS_QUEUE_NAME using a match expression
for easy extensibility to other queues in the future.
2026-02-03 09:24:32 +05:30
Prem Palanisamy cbe7ce7269 fix merge conflict in composer.lock 2026-02-02 21:48:20 +00:00
premtsd-codeandGitHub f2cfa4465f Merge branch '1.8.x' into chore-upgrade-cache 2026-02-02 21:03:45 +00:00
HemachandarandGitHub f648d82c8c Remove cleanup-stale-executions from Interval task (#11224) 2026-02-03 01:18:34 +05:30
Prem Palanisamy 8117ab9542 simplify CacheTest to match other Health tests 2026-02-02 19:07:50 +00:00
Prem Palanisamy 255098ed9c use env vars for cache reconnection tests 2026-02-02 18:36:16 +00:00
Prem Palanisamy 914be48e25 fix: add null check for cache adapter 2026-02-02 18:25:41 +00:00
premtsd-codeandGitHub 47e406baa0 Merge branch '1.8.x' into chore-upgrade-cache 2026-02-02 18:22:02 +00:00
ArnabChatterjee20k 3c2f0cc982 updated to new slot based filtering for realtime 2026-02-02 21:50:23 +05:30
Matej BačoandGitHub a967ee9b1b Merge pull request #11213 from appwrite/feat-array-domains-env
Feat: Array domain variables
2026-02-02 17:06:55 +01:00
ArnabChatterjee20k 6e576b5140 added realtime query new channel and channel wise filters 2026-02-02 19:45:21 +05:30
premtsd-codeandGitHub ab689af94b Merge branch '1.8.x' into chore-upgrade-cache 2026-02-02 13:21:22 +00:00
Prem Palanisamy e7b4f03227 improve cache reconnection tests 2026-02-02 13:19:41 +00:00
HemachandarandGitHub 630d3376a6 Use Swoole timers in Interval task (#11205)
* Use Swoole timers in `Interval` task

* refactor

* block coroutine from exiting

* nit

* Add span

* cleanup unused var

* camelCase
2026-02-02 18:47:54 +05:30
Jake BarnbyandGitHub a25977c6b8 Merge pull request #11222 from appwrite/release-array-param-enums 2026-02-02 13:08:51 +00:00
Chirag Aggarwal 19bfda541f Merge branch '1.8.x' into release-array-param-enums 2026-02-02 18:35:20 +05:30
Chirag Aggarwal 2ac265baea update composer 2026-02-02 18:34:24 +05:30
Prem Palanisamy a52f7f2485 fix: ensure Redis restart in cache reconnection tests 2026-02-02 12:57:40 +00:00
Prem Palanisamy 6876828750 add cache reconnection support 2026-02-02 12:49:58 +00:00
Chirag Aggarwal 02c92d95b7 examples and changelog 2026-02-02 18:04:02 +05:30
Chirag Aggarwal eb97a0e9b2 changelog changes 2026-02-02 17:57:50 +05:30
Chirag Aggarwal ae6a81bd12 update specs 2026-02-02 17:54:57 +05:30
premtsd-codeandGitHub 6f7da6d622 Merge branch '1.8.x' into chore-upgrade-cache 2026-02-02 12:23:42 +00:00
Prem Palanisamy e79dc39d5d chore: update composer.lock 2026-02-02 12:21:08 +00:00
Chirag Aggarwal 4d062b818d update examples 2026-02-02 17:26:20 +05:30
Prem Palanisamy a1dad882e4 chore: update composer.lock 2026-02-02 11:02:42 +00:00
Prem Palanisamy abb546d099 chore: replace dev dependencies with stable versions 2026-02-02 11:00:20 +00:00
Jake BarnbyandGitHub dfdbcf015c Merge pull request #11207 from appwrite/migration-endpoint 2026-02-02 10:34:04 +00:00
Chirag AggarwalandGitHub 29ab9ef9d2 Merge pull request #11223 from appwrite/update-message-logging 2026-02-02 15:46:43 +05:30
Chirag Aggarwal 52174816b2 chore: update messaging worker logging 2026-02-02 15:39:12 +05:30
Chirag Aggarwal 9ee318de3c feat: release array param enums support to all sdks 2026-02-02 15:34:32 +05:30
fogelito 2e5b232e2b line 2026-02-02 11:34:57 +02:00
fogelito 53b8886729 Remove env variable 2026-02-02 11:29:38 +02:00
fogelito e9e51d5334 lock 2026-02-02 11:27:50 +02:00
fogelito c1161ff27c check isset 2026-02-02 08:13:24 +02:00
Prem Palanisamy 1854d01e6e chore: upgrade utopia-php dependencies for cache reconnection support 2026-02-02 06:13:00 +00:00
fogelito f530ec3db5 Remove dbg 2026-02-01 15:57:30 +02:00
fogelito 3502cad4a5 formatting 2026-02-01 15:53:41 +02:00
fogelito 05e1bb242f stopOnFailure false 2026-02-01 15:52:29 +02:00
fogelito e760a1386b Use credentials 2026-02-01 15:51:41 +02:00
fogelito 644dcb8fb1 empty $credentials 2026-02-01 15:11:29 +02:00
fogelito 40dede6503 Use apiHostname 2026-02-01 14:54:42 +02:00
fogelito d19eec9849 var dump 2026-02-01 12:31:17 +02:00
fogelito 88044eaa71 check key 2026-02-01 12:15:32 +02:00
fogelito 6011a7c66d http 2026-02-01 12:00:04 +02:00
fogelito 771dd92a14 https 2026-02-01 11:55:01 +02:00
fogelito e1f8d0bbbc remove extra vars 2026-02-01 10:38:04 +02:00
fogelito 7da2bdf4da remove extra vars 2026-02-01 10:36:38 +02:00
fogelito d89539b56a Merge branch '1.8.x' of https://github.com/appwrite/appwrite into migration-endpoint 2026-02-01 08:49:01 +02:00
fogelito 23f08ef144 formatting 2026-02-01 08:46:39 +02:00
Matej Bačo ce1bb3fd29 ai review fixes 2026-01-30 22:23:36 +01:00
Matej Bačo fea4994ef0 ai review fixes 2026-01-30 21:30:00 +01:00
Matej Bačo 4807dcf529 Array support for all functions domain usecases 2026-01-30 16:48:40 +01:00
Matej Bačo 817475a251 Add tests 2026-01-30 16:24:41 +01:00
Matej Bačo 150c9033cc Support arrays in domains env variables 2026-01-30 16:20:46 +01:00
Matej BačoandGitHub 8938dcf8f4 Merge pull request #11212 from appwrite/chore-preview-domain-under-platform
Chore: Platform to defien preview domain
2026-01-30 14:29:17 +01:00
Matej Bačo edbec1ef41 AI review fixes 2026-01-30 13:29:28 +01:00
Matej Bačo edceb3e0db move preview domain under platform for future customizability 2026-01-30 13:23:37 +01:00
Jake BarnbyandGitHub d3974776be Merge pull request #11210 from appwrite/fix-events 2026-01-30 10:05:18 +00:00
HemachandarandGitHub 1110fdb719 Add appendVariables method to Mail event (#11166)
* Add `addVariable` method to Mail event

* append variables

* Update template
2026-01-30 15:20:53 +05:30
Jake Barnby 252dc6b993 Fix test 2026-01-30 22:44:19 +13:00
Jake Barnby 67e43cc1a5 Push vs merge 2026-01-30 21:48:36 +13:00
Jake Barnby d4b0ea64ad Fix event caching 2026-01-30 21:41:01 +13:00
fogelito a38b6cc78d lock 2026-01-30 10:05:11 +02:00
fogelito 9d7bc04089 throw 2026-01-30 10:04:09 +02:00
fogelito 5c61f7232a _APP_MIGRATION_ENDPOINT 2026-01-29 18:22:56 +02:00
fogelito 6e6081f693 set credentials 2026-01-29 17:33:37 +02:00
fogelito ddb72c7546 set credentials 2026-01-29 17:32:28 +02:00
fogelito 1b82d32436 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into migration-endpoint
# Conflicts:
#	.env
2026-01-29 17:15:05 +02:00
fogelito e8d40a48dc endpoint 2026-01-29 17:13:01 +02:00
Jake BarnbyandGitHub 6ccc8b9261 Merge pull request #11174 from appwrite/feat-string-types 2026-01-29 14:39:06 +00:00
Jake Barnby 0d79ab0fd3 Update specs 2026-01-30 02:21:32 +13:00
Jake Barnby c9aac86a6b Merge remote-tracking branch 'origin/1.8.x' into feat-string-types
# Conflicts:
#	composer.lock
2026-01-30 02:07:33 +13:00
Matej BačoandGitHub 8c626ea2ca Merge pull request #11206 from appwrite/chore-upgrade-templates
Chore: Upgrade templates version
2026-01-29 13:53:32 +01:00
Jake BarnbyandGitHub 9d953f898d Merge pull request #11007 from appwrite/new-pool-adapter 2026-01-29 12:44:13 +00:00
Matej BačoandGitHub c466ffcf7e Merge branch '1.8.x' into chore-upgrade-templates 2026-01-29 12:54:58 +01:00
Matej BačoandGitHub 366e578cf8 Merge pull request #11189 from appwrite/chore-usecases-enums
Chore: Proper quality to template usecases enums
2026-01-29 12:49:38 +01:00
Matej Bačo 63ceea7aca Fix wrong ref versions 2026-01-29 12:30:47 +01:00
Matej BačoandGitHub 1df09c652d Merge branch '1.8.x' into chore-usecases-enums 2026-01-29 12:12:17 +01:00
ArnabChatterjee20k 6b9f48aa76 updated lock 2026-01-29 16:32:49 +05:30
ArnabChatterjee20k c8c5fbaca2 updated lock 2026-01-29 16:18:23 +05:30
Matej Bačo 1131e4ed18 Revert "Upgrade template versions"
This reverts commit a2d920195d.
2026-01-29 11:46:05 +01:00
Matej Bačo a2d920195d Upgrade template versions 2026-01-29 11:39:42 +01:00
ArnabChatterjee20k 5753c7694d updated lock 2026-01-29 14:52:16 +05:30
ArnabChatterjee20k 0d057390ab updated composer and docker compose 2026-01-29 14:34:20 +05:30
ArnabChatterjee20k e82345be40 updated tests 2026-01-29 14:22:08 +05:30
ArnabChatterjee20k 077068e9fc updated tests 2026-01-29 14:20:05 +05:30
ArnabChatterjee20k 64392c1520 fixed failing tests 2026-01-29 11:38:20 +05:30
ArnabChatterjee20kandGitHub 7973824321 Merge branch '1.8.x' into new-pool-adapter 2026-01-29 11:20:02 +05:30
Jake BarnbyandGitHub b1ca73a041 Merge pull request #11193 from premtsd-code/fix-10612-validate-relationship-document-id 2026-01-29 04:56:47 +00:00
premtsd-codeandGitHub 845aa5c363 Merge branch '1.8.x' into fix-10612-validate-relationship-document-id 2026-01-28 18:45:53 +00:00
Prem Palanisamy 23dae85d2f Sync composer.lock with 1.8.x 2026-01-28 18:45:11 +00:00
Luke B. SilverandGitHub 7bf02811f2 Merge pull request #11198 from appwrite/refactor-remove-magic-class-strings
refactor: remove magic class strings
2026-01-28 16:56:31 +00:00
premtsd-codeandGitHub 23c1f8fd5a Merge branch '1.8.x' into fix-10612-validate-relationship-document-id 2026-01-28 15:21:40 +00:00
Matej BačoandGitHub b9cb81496d Merge pull request #11203 from appwrite/chore-deprecate-project-attributes
Chore: Remove unnessessary attributes
2026-01-28 15:58:58 +01:00
Matej Bačo e22e8d6a5f Upgrade phpunit for vuln 2026-01-28 14:55:13 +01:00
ArnabChatterjee20kandGitHub 24385355a0 Merge branch '1.8.x' into new-pool-adapter 2026-01-28 19:24:59 +05:30
ArnabChatterjee20k 44b52b5a00 updated swoole and pools 2026-01-28 19:24:33 +05:30
ArnabChatterjee20k 6c29dd055c updated php unit 2026-01-28 19:21:01 +05:30
Matej Bačo 14a96a2b56 Remove unnessessary attributes 2026-01-28 14:50:17 +01:00
Matej BačoandGitHub 273364114a Merge pull request #10988 from appwrite/chore-php-types
Feat: CE support for Platform API
2026-01-28 14:45:42 +01:00
ArnabChatterjee20kandGitHub 72db0c721b Merge branch '1.8.x' into realtime-query-subscriptions 2026-01-28 19:06:30 +05:30
ArnabChatterjee20k cbe13d71f1 removed redundant key 2026-01-28 19:04:42 +05:30
ArnabChatterjee20k b785dea7ac added query per subscription and queryKeys along with the messages 2026-01-28 18:40:30 +05:30
Prem Palanisamy 2f3fa9e0d3 sync CONTRIBUTING.md with 1.8.x 2026-01-28 11:42:51 +00:00
premtsd-codeandGitHub 71670f8169 Merge branch '1.8.x' into fix-10612-validate-relationship-document-id 2026-01-28 11:17:50 +00:00
Matej BačoandGitHub a4a589c535 Merge branch '1.8.x' into chore-php-types 2026-01-28 11:44:51 +01:00
Prem Palanisamy cbe2d2383d chore: update phpunit to 9.6.34 (security fix) 2026-01-28 10:24:11 +00:00
Prem Palanisamy aef7b8df38 fix: use RELATIONSHIP_VALUE_INVALID exception for validation errors 2026-01-28 08:41:10 +00:00
ArnabChatterjee20k 125394166a updated docker appwrite image base 2026-01-28 12:04:26 +05:30
ArnabChatterjee20k 68cb03d22c updated to swoole 6 2026-01-27 19:16:57 +05:30
Prem Palanisamy 7f3ea98924 refactor: use array_is_list() and assertEventually helper 2026-01-27 13:00:29 +00:00
loks0n cb66e50612 refactor: remove magic class strings 2026-01-27 11:58:34 +00:00
ArnabChatterjee20k 87db62b018 updated env 2026-01-27 16:39:08 +05:30
ArnabChatterjee20k 5fe5445283 Merge remote-tracking branch 'origin/1.8.x' into new-pool-adapter 2026-01-27 16:37:58 +05:30
ArnabChatterjee20k 303fca0fd5 updated pools 2026-01-27 16:36:28 +05:30
HemachandarandGitHub 99dc31062d Fix rule status check (#11195) 2026-01-27 15:36:50 +05:30
Prem Palanisamy d182c85330 fix: reject unsupported relationship value types 2026-01-27 09:35:45 +00:00
Prem Palanisamy d792d3bbea refactor: use getId() instead of getAttribute('$id') 2026-01-27 09:25:39 +00:00
Prem Palanisamy 00d091513d refactor: simplify relationship validation code 2026-01-27 06:59:53 +00:00
Prem Palanisamy 63e6a51af1 test: add assertion for relationship column polling 2026-01-26 15:45:42 +00:00
Prem Palanisamy 1ee2539ce0 fix: generate unique ID before validation per coderabbit suggestion 2026-01-26 15:45:42 +00:00
premtsd-codeandGitHub cb32dc40ec Merge branch 'main' into fix-10612-validate-relationship-document-id 2026-01-26 14:06:44 +00:00
Prem Palanisamy f66e0c2ff5 refactor: separate validation from normalization in validateRelationship 2026-01-26 14:05:56 +00:00
Prem Palanisamy aed9816d1e fix: validate relationship document ID 2026-01-26 13:48:32 +00:00
Chirag AggarwalandGitHub 6514a992a1 Merge pull request #11078 from appwrite/feat-sdk-for-md 2026-01-26 13:06:10 +05:30
Chirag Aggarwal 1167654c6a Merge branch '1.8.x' into feat-sdk-for-md 2026-01-26 13:03:02 +05:30
Chirag AggarwalandGitHub 28c5df074d Merge pull request #11169 from appwrite/release-cli-13.1.0 2026-01-26 12:59:35 +05:30
Chirag Aggarwal 785efa3355 stable release 2026-01-26 10:40:29 +05:30
Chirag Aggarwal 329b7e1856 Merge branch '1.8.x' into feat-sdk-for-md 2026-01-26 10:25:28 +05:30
Matej Bačo 752731050e Proper quality to template usecases enums 2026-01-25 21:55:52 +01:00
Jake Barnby 368113ade5 Merge branch 'feat-string-types' of github.com:appwrite/appwrite into feat-string-types
# Conflicts:
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Create.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Update.php
2026-01-24 16:24:35 +13:00
Jake Barnby f1d1c5a737 Merge remote-tracking branch 'origin/1.8.x' into feat-string-types
# Conflicts:
#	composer.lock
2026-01-24 16:23:39 +13:00
Jake Barnby 5815008994 Update deprecation 2026-01-24 16:23:05 +13:00
Jake BarnbyandGitHub ac83d417aa Merge pull request #11183 from appwrite/copilot/sub-pr-11174 2026-01-24 14:47:34 +13:00
copilot-swe-agent[bot]andabnegate 9993c15fb8 chore: revert String attribute routes deprecation to 1.8.0
Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-01-24 01:15:15 +00:00
copilot-swe-agent[bot]andabnegate 11aecdf33f chore: mark TablesDB String column routes as deprecated
Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-01-24 01:11:03 +00:00
copilot-swe-agent[bot]andabnegate a088e85a24 chore: update String attribute deprecation to 1.9.0
Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-01-24 01:01:49 +00:00
copilot-swe-agent[bot]andabnegate 1fd687b6b7 chore: update composer.lock after reverting to 4.*
Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-01-24 00:30:56 +00:00
copilot-swe-agent[bot]andabnegate aa26823549 chore: revert utopia-php/database to 4.*
Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-01-24 00:26:11 +00:00
copilot-swe-agent[bot] 109e16362d Initial plan 2026-01-24 00:23:12 +00:00
Jake BarnbyandGitHub 294518d014 Merge branch '1.8.x' into feat-string-types 2026-01-24 13:21:30 +13:00
Jake BarnbyandClaude Opus 4.5 046a7cd90e test: skip update tests pending utopia-php/database fix
The updateAttribute method in utopia-php/database does not yet support
VARCHAR, TEXT, MEDIUMTEXT, and LONGTEXT types. These tests are skipped
until the upstream library adds support for updating these attribute types.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 06:15:21 +13:00
Jake BarnbyandClaude Opus 4.5 fea210c9ed chore: use utopia-php/database dev-feat-string-types branch
The string types feature requires the dev-feat-string-types branch of
utopia-php/database which includes support for VARCHAR, TEXT, MEDIUMTEXT,
and LONGTEXT types in the updateAttribute method.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 05:51:28 +13:00
Jake BarnbyandClaude Opus 4.5 760f065711 fix: reduce longtext size to fit within INT column limit
The size field in the attributes collection is stored as a signed 32-bit
integer (VAR_INTEGER). The longtext size of 4294967295 (2^32-1) exceeds
the maximum value of 2147483647 (2^31-1), causing attribute creation to
fail with a 400 error (document_invalid_structure).

Changed the longtext size to 2147483647 which is the maximum value that
fits within the signed 32-bit integer constraint of the schema.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 05:24:26 +13:00
Jake BarnbyandClaude Opus 4.5 c42fbc49f5 fix: remove max-size varchar test that exceeds row width limit
The test tried to create multiple VARCHAR attributes in the same
collection, including a 16381-character varchar. The cumulative
row width exceeded MariaDB's 65535 byte row limit, causing the
test to fail with a 400 error.

Calculation: 1067 (base) + 1021 (255*4+1) + 401 (100*4+1) + 201
(50*4+1) + 20 (array) + 65526 (16381*4+2) = 68236 bytes > 65535

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 04:25:06 +13:00
Chirag AggarwalandGitHub 0dd2f29a7e Merge pull request #11182 from appwrite/chore-make-update-name-optional 2026-01-23 20:51:36 +05:30
Jake BarnbyandClaude Opus 4.5 e20851e7a8 fix: reorder imports to satisfy linter
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 03:53:42 +13:00
Chirag Aggarwal 67e2104e98 chore: make name update optional in databases and tables 2026-01-23 19:55:06 +05:30
HemachandarandGitHub a2ff833efe Upgrade utopia-php/domains (#11181)
* Upgrade utopia-php/domains

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

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

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

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

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

Changes:
- Add 'format: int64' to min/max fields in AttributeInteger and ColumnInteger models
- Regenerate OpenAPI 3.x and Swagger 2.x specs for all platforms (client, console, server)
- Update composer dependencies
2026-01-13 17:00:20 +05:30
Darshan 23d84cf29f update: dart sdk. 2026-01-13 14:21:09 +05:30
Darshan c79019f960 regen: specs. 2026-01-13 13:58:57 +05:30
Chirag AggarwalandGitHub dd78b6dbce Merge branch '1.8.x' into release-cli-13.0.0.rc3 2026-01-13 13:39:27 +05:30
Chirag Aggarwal 81e64afe7a changelog 2026-01-13 13:38:44 +05:30
Chirag Aggarwal 5ef675bcdd release cli sdk 13.0.0 rc5 2026-01-13 13:38:32 +05:30
Jake BarnbyandGitHub a4734a5de7 Merge pull request #11099 from appwrite/feat-auth-instance 2026-01-13 20:35:22 +13:00
Jake Barnby 03812cf7bf Lint 2026-01-13 18:59:55 +13:00
Jake Barnby 515a34c180 Set health adapter auth 2026-01-13 18:52:16 +13:00
Jake Barnby 8e688bad4d Revert ping 2026-01-13 18:36:38 +13:00
Damodar Lohani 4905f7cc48 Merge remote-tracking branch 'origin/1.8.x' into feat-health-module 2026-01-13 05:19:00 +00:00
Jake Barnby 3d27b76051 Fix avatars 2026-01-13 18:05:46 +13:00
Jake Barnby 2d6348bf5d Fix health 2026-01-13 17:22:05 +13:00
Jake Barnby 2131a0cea1 Sync auth 2026-01-13 16:21:43 +13:00
Jake Barnby 9379e4d6c8 Merge remote-tracking branch 'origin/1.8.x' into feat-auth-instance
# Conflicts:
#	app/controllers/api/avatars.php
#	composer.lock
#	src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
2026-01-13 15:46:23 +13:00
Jake BarnbyandGitHub 15922fb88c Merge pull request #11103 from appwrite/avatars-module 2026-01-12 21:28:56 +13:00
Jake BarnbyandGitHub ba9526cce0 Merge pull request #11118 from appwrite/bump-http 2026-01-12 21:16:15 +13:00
Darshan 6dbed494e3 bump: swoole and framework. 2026-01-12 13:29:50 +05:30
Darshan 4eb1e7ae45 Merge branch '1.8.x' into avatars-module 2026-01-12 12:04:56 +05:30
Darshan bdd53212a0 Merge branch '1.8.x' into avatars-module 2026-01-12 12:04:41 +05:30
Darshan 1a69d9d236 remove: avatars controller. 2026-01-12 12:03:58 +05:30
Jake BarnbyandGitHub a860c33969 Merge pull request #11115 from appwrite/delete-subscribers 2026-01-12 18:50:59 +13:00
shimon ffd73dc70f Merge remote-tracking branch 'origin/add-webhooks-and-functions-events' into add-webhooks-and-functions-events 2026-01-11 14:12:22 +02:00
shimon 362a7bef0e Merge branch '1.8.x' of github.com:appwrite/appwrite into add-webhooks-and-functions-events 2026-01-11 14:11:51 +02:00
fogelito d08a6f572c message 2026-01-11 08:30:52 +02:00
fogelito 893c1aa669 formatting 2026-01-11 08:21:30 +02:00
fogelito c34873a8b0 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into delete-subscribers 2026-01-11 08:17:40 +02:00
fogelito 17f60b611b catch 2026-01-11 08:13:39 +02:00
Matej Bačo cd0d609229 quality improv 2026-01-10 17:01:31 +01:00
Matej Bačo 497e5f8d00 tests fixes 2026-01-10 16:57:23 +01:00
Matej Bačo 60e1efb8cb Merge conflict fix 2026-01-10 16:42:45 +01:00
Matej Bačo 3d4b29e0f1 Merge branch '1.8.x' into chore-php-types 2026-01-10 16:42:38 +01:00
Matej Bačo dad21a912e PR review changes 2026-01-10 16:35:09 +01:00
Matej BačoandGitHub 445d7495a3 Merge pull request #11110 from appwrite/feat-async-screenshots
Feat: Async screenshots
2026-01-09 14:35:20 +01:00
Matej Bačo aebbe91c34 formatting fix 2026-01-09 11:25:47 +01:00
Matej Bačo 2a9f9f6851 Fix race condition with screenshot worker updating dpeloyment 2026-01-09 11:25:39 +01:00
Matej Bačo eec023aab3 Simplify screenshot failures 2026-01-09 11:25:25 +01:00
Matej Bačo 71f389e1c0 AI PR review 2026-01-09 10:41:58 +01:00
Matej Bačo d71b289025 Implement screenshot worker 2026-01-08 16:51:04 +01:00
Damodar Lohani f4da9b54e7 improve get certificate 2026-01-08 06:34:11 +00:00
Damodar LohaniandGitHub df84b94df0 Merge branch '1.8.x' into feat-health-module 2026-01-08 12:08:57 +05:45
Damodar Lohani 42bf515c8a Fix typos 2026-01-08 06:21:57 +00:00
Jake BarnbyandGitHub a7d39b5ef7 Merge pull request #11105 from appwrite/bump-migrations 2026-01-08 19:15:29 +13:00
Jake Barnby 4c1417944d Fix bucket stats auth 2026-01-08 18:54:52 +13:00
Damodar LohaniandGitHub 27aee1fb88 Merge pull request #11054 from appwrite/feat-file-create-after-success-hook
Feat: add after success hook for file creation endpoint
2026-01-08 11:31:14 +05:45
Darshan 4319c16584 bump: migrations. 2026-01-08 11:13:16 +05:30
Damodar LohaniandGitHub d68fc9a22e Merge branch '1.8.x' into feat-health-module 2026-01-08 11:26:56 +05:45
Jake Barnby e3d6fc123f Update lock 2026-01-08 18:11:37 +13:00
Jake Barnby e6505b9cf9 Merge remote-tracking branch 'origin/1.8.x' into feat-auth-instance
# Conflicts:
#	composer.lock
2026-01-08 17:48:37 +13:00
Jake Barnby c1ab9da740 Update migrations 2026-01-08 17:48:20 +13:00
Jake Barnby b567cd5341 Update storage module for auth instance 2026-01-08 15:01:30 +13:00
Luke B. SilverandGitHub f2ac01bccf Merge pull request #11104 from appwrite/feat-graceful-workers
feat: graceful workers
2026-01-07 23:23:44 +00:00
loks0n eecfba2a72 feat: graceful workers 2026-01-07 16:50:53 +00:00
shimon 2cfaa2223e feat: inject EventProcessor into Update transaction for enhanced event handling 2026-01-07 18:19:20 +02:00
shimon 1a87bde88e removing blank lines 2026-01-07 17:25:12 +02:00
shimon 4b92e3781f removing blank lines 2026-01-07 17:24:53 +02:00
shimon 65bb8946f6 Merge branch '1.8.x' of github.com:appwrite/appwrite into add-webhooks-and-functions-events 2026-01-07 16:58:23 +02:00
shimon db4dcd164e refactor: integrate EventProcessor for handling function and webhook events; streamline event triggering in database actions 2026-01-07 16:57:57 +02:00
Darshan 384c47f436 Merge remote-tracking branch 'origin/avatars-module' into avatars-module 2026-01-07 19:42:56 +05:30
Darshan dcc926402d fix: path error. 2026-01-07 19:42:50 +05:30
Darshan 6969e446d5 Merge branch '1.8.x' into avatars-module 2026-01-07 19:41:49 +05:30
DarshanandGitHub c8bb939357 Merge branch '1.8.x' into avatars-module 2026-01-07 19:37:28 +05:30
Darshan 02488c853d fix: paths error. 2026-01-07 19:28:44 +05:30
Darshan 92f26c1b5a update: move avatars controller to module structure. 2026-01-07 19:09:56 +05:30
DarshanandGitHub ded06d7435 Merge pull request #11102 from appwrite/add-xlist-totalsize 2026-01-07 18:53:53 +05:30
Matej BačoandGitHub 6c59cef8f4 Merge pull request #11090 from appwrite/fix-project-search
Fix: Allows query search on project
2026-01-07 14:09:56 +01:00
Darshan 46072bb95e fix: test. 2026-01-07 18:32:03 +05:30
Darshan 904b7312d6 address comments. 2026-01-07 18:22:36 +05:30
DarshanandGitHub 68fc6b0600 Merge branch '1.8.x' into add-xlist-totalsize 2026-01-07 18:08:59 +05:30
Darshan 47ea8699d1 add: tests. 2026-01-07 18:05:33 +05:30
Darshan 9ba3f6dfe8 add: totalsize on xlist. 2026-01-07 18:00:04 +05:30
HemachandarandGitHub 3483daef0d Stop publishing rule verification errors to Sentry (#11101) 2026-01-07 17:33:00 +05:30
DarshanandGitHub 9943305640 Merge pull request #11100 from appwrite/fix-dat-1019 2026-01-07 16:40:58 +05:30
DarshanandGitHub 8bb6f01e22 Merge pull request #11069 from appwrite/storage-size 2026-01-07 16:36:06 +05:30
Darshan 2cc7bbc0a4 update: address comment, inline method. 2026-01-07 16:00:52 +05:30
Jake Barnby e9366ac2e6 Merge remote-tracking branch 'origin/1.8.x' into feat-auth-instance
# Conflicts:
#	composer.json
#	composer.lock
#	src/Appwrite/Platform/Workers/Migrations.php
2026-01-07 23:26:41 +13:00
Jake Barnby 22f8a3eab9 Sync merge 2026-01-07 23:24:49 +13:00
Darshan 896e5a517a lint. 2026-01-07 15:42:51 +05:30
Darshan 281dcfc64a add queries to logging. 2026-01-07 15:37:56 +05:30
DarshanandGitHub 457cd53475 Merge branch '1.8.x' into storage-size 2026-01-07 14:00:36 +05:30
Darshan 93e7dc772d Merge remote-tracking branch 'origin/storage-size' into storage-size 2026-01-07 13:57:59 +05:30
Darshan 228d095ee5 address comment and fix tests. 2026-01-07 13:57:41 +05:30
Jake BarnbyandGitHub 43189f8940 Merge pull request #11086 from appwrite/migrations-cleanup 2026-01-07 21:15:38 +13:00
DarshanandGitHub b615e4b901 Merge branch '1.8.x' into storage-size 2026-01-07 13:43:48 +05:30
Darshan ebd6457361 address comment. 2026-01-07 13:39:51 +05:30
fogelito 0d3bcc9b3a message todo 2026-01-07 09:52:23 +02:00
fogelito 61e98a501a composer migration 1.3.* 2026-01-07 09:43:13 +02:00
fogelito 502012ddf7 composer migration 1.3.* 2026-01-07 09:42:21 +02:00
fogelito a6eb479c93 composer migration 2026-01-07 09:38:58 +02:00
fogelito e0d7b418dc Merge branch '1.8.x' of https://github.com/appwrite/appwrite into migrations-cleanup
# Conflicts:
#	composer.lock
2026-01-07 09:37:48 +02:00
Jake Barnby ea97072479 Merge remote-tracking branch 'origin/1.8.x' into feat-auth-instance
# Conflicts:
#	composer.lock
2026-01-07 20:10:26 +13:00
Jake Barnby 7573ee75a2 Use authorization instance 2026-01-07 20:04:28 +13:00
Damodar LohaniandGitHub 0dc00eb2c5 Merge pull request #11095 from appwrite/lohanidamodar-patch-2
Fix deleteAuditLogs function call parameters
2026-01-07 12:41:43 +05:45
Jake BarnbyandGitHub b8ccc3b846 Merge pull request #11096 from appwrite/bump-migrations-lib 2026-01-07 19:18:52 +13:00
Damodar LohaniandGitHub 6f1cac7d76 Merge branch '1.8.x' into feat-health-module 2026-01-07 11:53:34 +05:45
Darshan 5f3384b821 bump. 2026-01-07 11:24:15 +05:30
Darshan 71cb462f49 Merge branch '1.8.x' into 'bump-migrations-lib'. 2026-01-07 11:20:00 +05:30
Darshan 27e859030d use: vcs for now. 2026-01-07 11:15:28 +05:30
Damodar LohaniandGitHub e7a82e4d31 Fix deleteAuditLogs function call parameters 2026-01-07 11:08:26 +05:45
Jake BarnbyandGitHub 6dc836307e Merge pull request #11088 from appwrite/delete-project-skip-validations 2026-01-07 18:10:44 +13:00
shimon 0582cdf394 refactor: clean up whitespace and remove commented-out abuse handling code in api.php 2026-01-06 18:41:06 +02:00
shimon 573d8423a3 refactor: remove unused purgeFunctionEventsCache method and clean up whitespace in Update class 2026-01-06 18:40:17 +02:00
shimon 3fa3318346 Merge branch '1.8.x' of github.com:appwrite/appwrite into add-webhooks-and-functions-events 2026-01-06 18:32:00 +02:00
shimon 23dfb23a3b fix: revert Traefik image version to 2.11; implement caching for function events and webhooks; add cache purging on function create/update/delete events 2026-01-06 18:28:37 +02:00
HemachandarandGitHub fc5ea06821 Bump utopia-php/fetch version (#10997)
* Bump utopia-php/fetch version

* fix timeouts
2026-01-06 21:30:25 +05:30
fogelito 5642983f91 Pull main 2026-01-06 17:26:30 +02:00
fogelito b208e5c066 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into migrations-cleanup
# Conflicts:
#	composer.json
#	composer.lock
2026-01-06 17:26:08 +02:00
Matej BačoandGitHub aa135d5cd4 Merge pull request #11085 from appwrite/feat-success-abuse-reset
Feat: Abuse reset on success
2026-01-06 15:56:36 +01:00
fogelito 6756ee31b6 migration can not be empty 2026-01-06 16:55:36 +02:00
fogelito 89c988d73c finally try catch 2026-01-06 16:36:34 +02:00
Matej Bačo 6d85d1567e Update composer.lock 2026-01-06 15:33:21 +01:00
Matej Bačo 7567639996 grammar fix 2026-01-06 15:18:44 +01:00
Matej Bačo 057bff2140 Merge branch '1.8.x' into feat-success-abuse-reset 2026-01-06 15:18:25 +01:00
Matej Bačo d7bb234072 PR reviews 2026-01-06 15:18:22 +01:00
Matej Bačo c0f8dee4d4 Allows query search on project 2026-01-06 15:03:43 +01:00
fogelito eb10996517 Add try finally 2026-01-06 15:36:08 +02:00
fogelito 31803f0eb9 message 2026-01-06 15:30:19 +02:00
fogelito ac9214f3c4 revert 2026-01-06 14:57:54 +02:00
fogelito 14347d86a8 try disableValidation 2026-01-06 14:49:19 +02:00
fogelito 1b855d2d41 disables validations 2026-01-06 14:37:46 +02:00
HemachandarandGitHub 3b5b15d1a6 Remove dual read for keys (#11083)
* Remove dual read for `keys`

* write to mock

* remove dual writes

* Revert "remove dual writes"

This reverts commit ce9a48423b.

* add todo
2026-01-06 17:49:32 +05:30
Shmuel FogelandGitHub 2c5bc32f7e Merge pull request #11072 from appwrite/platform-rules-collections
Sync platform rules + common targets tables
2026-01-06 13:59:41 +02:00
fogelito 59b41c4b52 lock 2026-01-06 11:16:34 +02:00
fogelito b6e8b55994 Throw 2026-01-06 11:01:38 +02:00
fogelito 3a4fb5dd14 throws 2026-01-06 10:56:33 +02:00
fogelito 2a679ead32 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into migrations-cleanup 2026-01-06 10:45:19 +02:00
fogelito 7da9d480d0 cleanUp 2026-01-06 10:43:16 +02:00
Matej Bačo a37aa2dd68 Generic abuse test group 2026-01-05 22:51:11 +01:00
Matej Bačo b29f70a0af Add new tests 2026-01-05 22:51:03 +01:00
Matej Bačo 2fc5e56a61 WIP: Abuse reset on success 2026-01-05 22:05:00 +01:00
Matej BačoandGitHub b1373c02bc Merge pull request #11056 from appwrite/feat-project-labels
Feat: Project labels
2026-01-05 17:15:30 +01:00
Matej Bačo 1db12e78ef AI code review 2026-01-05 14:42:03 +01:00
Matej Bačo a25c8004e4 Merge branch '1.8.x' into feat-project-labels 2026-01-05 14:32:26 +01:00
Chirag AggarwalandGitHub 547d59d95e Merge pull request #11079 from appwrite/validate-smtp-connection 2026-01-05 17:36:58 +05:30
Chirag Aggarwal 4e0477af32 add timeout to mailer 2026-01-05 17:25:12 +05:30
Luke B. SilverandGitHub 1659bc3ffa Merge pull request #11080 from appwrite/fix-audit-delete
Fix audit delete
2026-01-05 10:53:52 +00:00
fogelito 84e9c8243c fix param order 2026-01-05 09:30:47 +02:00
Chirag Aggarwal 001d38caf5 feat: sdk for md 2026-01-05 12:53:35 +05:30
fogelito 020d7bb801 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into platform-rules-collections 2026-01-04 12:26:49 +02:00
fogelito b6aeaffe8b Remove region index 2026-01-04 12:20:22 +02:00
fogelito 65883122e3 rules changes 2026-01-04 12:16:59 +02:00
shimon cd651dbdb8 chore: update dependencies and fix formatting issues in composer files; change Traefik image version in docker-compose; add debug output in Action.php 2026-01-04 11:35:20 +02:00
shimon 2dfb386b12 Merge branch '1.8.x' of github.com:appwrite/appwrite into add-webhooks-and-functions-events 2026-01-04 10:39:35 +02:00
shimon e9dac6710f Refactor: Remove unused webhook and function event filters, implement caching for function events retrieval 2026-01-04 09:53:29 +02:00
DarshanandGitHub d4660a70c3 Merge pull request #11071 from appwrite/fix-missing-dbId 2026-01-04 12:37:54 +05:30
Darshan 16f9c35850 update: tests. 2026-01-04 12:09:11 +05:30
Darshan 49511ebdd9 fix: missing database id in response. 2026-01-04 12:05:55 +05:30
Luke B. SilverandGitHub 63511c56ae Merge pull request #11067 from appwrite/fix-memory-leak
fix: memory leak
2026-01-03 23:02:31 +00:00
loks0n 445ada0226 fix: memory leak 2026-01-03 22:38:17 +00:00
Darshan a5d4f69c6c bump: specs 2026-01-03 15:43:54 +05:30
Darshan f788fc8f8a add: tests. 2026-01-03 15:42:55 +05:30
Darshan 67ef2ab552 add: return bucket actual size. 2026-01-03 15:13:34 +05:30
Steven NguyenandGitHub f0191202a0 Merge pull request #10342 from tejas-raskar/doc-10339-fix-inconsistent-dir-structure-in-contribution-docs
docs: update the directory structure in CONTRIBUTING.md
2026-01-02 15:59:24 -08:00
Steven NguyenandGitHub 68b4a49e9d Merge pull request #10896 from appwrite/feat-1.8.1-release-prep
Prepare 1.8.1 release
2026-01-02 15:38:50 -08:00
DarshanandGitHub 28a09d7aaa Merge pull request #11065 from appwrite/add-logging-tests 2026-01-02 17:20:10 +05:30
Darshan ee3b2d75ae add: missing test at console level. 2026-01-02 17:01:03 +05:30
Chirag AggarwalandGitHub ffe1fd89a7 Merge pull request #11064 from appwrite/chore-cookie-console 2026-01-02 16:29:39 +05:30
Chirag Aggarwal 06c4ba81e9 chore: sync specs + allow cookie auth in console platform 2026-01-02 15:20:45 +05:30
Chirag AggarwalandGitHub f2772cea6b Merge pull request #11063 from appwrite/fix-remove-production-attribute 2026-01-02 12:22:42 +05:30
Chirag Aggarwal b76a4bdd2f fix: remove production attribute when releasing sdks 2026-01-02 12:16:50 +05:30
Shmuel FogelandGitHub 1c94381830 Merge pull request #11058 from appwrite/cli-previous-errors
Cli previous errors
2026-01-01 17:36:24 +02:00
fogelito 0209d40a38 Cli previous errors 2026-01-01 12:17:20 +02:00
Damodar Lohani 297fae8f81 refactor tests 2026-01-01 09:54:13 +00:00
Damodar Lohani 3e403194e4 Fix tests 2026-01-01 09:37:10 +00:00
Damodar Lohani 0305790e69 Fix: update health status model to use HEALTH_STATUS_LIST 2026-01-01 07:59:06 +00:00
Damodar Lohani 25435aaa11 Error handling 2026-01-01 07:58:18 +00:00
Damodar Lohani 19895e54e3 Fix: health status returning ping in incorrect unit 2026-01-01 07:37:09 +00:00
Damodar Lohani 4b3fe0b6fe Fix missing brace 2026-01-01 07:35:28 +00:00
Damodar LohaniGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
4ef906b836 Update src/Appwrite/Platform/Modules/Health/Http/Health/Certificate/Get.php
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-01-01 13:15:28 +05:45
Damodar Lohani 28aa4e8a8d refactor and new endpoint test 2026-01-01 05:49:20 +00:00
Damodar Lohani 9c6a6c265a format 2026-01-01 05:45:14 +00:00
Damodar Lohani dc0eb5f7a7 Feat: Health module 2026-01-01 05:45:06 +00:00
Steven NguyenandGitHub 06367eca69 Merge pull request #10954 from appwrite/optional-assistant
Optional assistant
2025-12-31 17:14:37 -08:00
Matej Bačo 8d1acef95d Implement project labels 2025-12-31 15:44:18 +01:00
Matej BačoandGitHub 55343379f2 Merge pull request #11046 from appwrite/chore-module-docs
Chore: Add module docs
2025-12-31 15:19:23 +01:00
DarshanandGitHub a66b565150 Merge branch '1.8.x' into optional-assistant 2025-12-31 19:21:37 +05:30
Darshan ff160871ae update: check assistant status on upgrade as well. 2025-12-31 19:21:19 +05:30
Darshan 92a3eca194 Merge branch '1.8.x' into optional-assistant 2025-12-31 19:09:54 +05:30
Damodar Lohani 03ccca2c35 Add after create success hook in file creation process 2025-12-31 13:05:41 +00:00
Shmuel FogelandGitHub 07135956df Merge pull request #11053 from appwrite/skip-migrations-shutdown
skip migration shutdown
2025-12-31 14:27:41 +02:00
fogelito b5a363a2a7 skip shutdown 2025-12-31 14:01:56 +02:00
Matej BačoandGitHub 31dc9ff873 Merge pull request #11052 from appwrite/chore-upgrade-vcs-version
Chore: Upgrade VCS lib with getCommit fix
2025-12-31 12:22:11 +01:00
Matej Bačo 408f9c2d39 Upgrade VCS with getCommit fix 2025-12-31 11:54:22 +01:00
Matej Bačo 910bd69b16 Ai review fixes 2025-12-31 10:57:10 +01:00
DarshanandGitHub dfd5965d25 Merge pull request #11051 from appwrite/fix-resources 2025-12-31 15:25:55 +05:30
Darshan 2a38eedc98 fix: flaky test!
fix: get resource report what we want.
2025-12-31 15:00:23 +05:30
DarshanandGitHub f875b965be Merge pull request #11049 from appwrite/console-module 2025-12-31 13:20:24 +05:30
Darshan 2e57e5a868 fix: url. 2025-12-31 11:59:01 +05:30
Darshan 69ce99d1b4 feat: console module. 2025-12-31 11:52:41 +05:30
Chirag AggarwalandGitHub 6b881d4aaf Merge pull request #11045 from appwrite/reduce-sdk-release-steps 2025-12-31 10:01:11 +05:30
Damodar LohaniandGitHub aad7d6dbca Merge pull request #10953 from appwrite/feat-audits-upgrade
Feat: Audits  upgrade
2025-12-31 06:55:34 +05:45
Matej Bačo 33ffe4aca7 AI suggestions 2025-12-30 15:13:22 +01:00
Matej Bačo 77eb3d4bcd Update AGENTS.md 2025-12-30 15:12:57 +01:00
Matej Bačo 49989f38f7 Update AGENTS.md 2025-12-30 15:09:47 +01:00
Matej Bačo 18a49ccc44 Update grammar 2025-12-30 15:07:54 +01:00
Matej Bačo 73173a7f8c Add module docs 2025-12-30 15:04:26 +01:00
Chirag Aggarwal e3412bc554 chore: reduce sdk release steps 2025-12-30 18:25:11 +05:30
HemachandarandGitHub c1e50c7abd Write to new resource attributes in keys (#11003)
* Write to new resource attributes in `keys`

* temp for tests

* list keys

* add subqueries

* lint
2025-12-30 17:09:59 +05:30
Luke B. SilverandGitHub 6499f436ca Merge pull request #11040 from appwrite/fix-task-subprocesses
fix: task subprocesses
2025-12-30 09:37:49 +00:00
loks0n 55fbd77bf1 fix: task subprocesses 2025-12-30 09:13:26 +00:00
Luke B. SilverandGitHub 4046829fc6 Merge pull request #11039 from appwrite/chore-remove-proxy-containaer
chore: remove proxy containaer
2025-12-30 08:46:27 +00:00
Chirag AggarwalandGitHub 57e218266f Merge pull request #11043 from appwrite/fix-include-services 2025-12-30 14:09:19 +05:30
Chirag AggarwalandGitHub 188dd5c423 Merge branch '1.8.x' into fix-include-services 2025-12-30 13:51:15 +05:30
Chirag Aggarwal d8d3f22883 fix: remove storage service from include_once 2025-12-30 13:42:39 +05:30
ArnabChatterjee20kandGitHub f338a819e1 Merge branch '1.8.x' into new-pool-adapter 2025-12-30 13:23:01 +05:30
ArnabChatterjee20k 2e82f0c2ec fix: update coroutine pool handling and configuration 2025-12-30 13:03:59 +05:30
Damodar LohaniandGitHub 15dc316535 Merge branch '1.8.x' into feat-audits-upgrade 2025-12-30 07:44:10 +05:45
loks0n 7581019e76 chore: remove proxy container 2025-12-29 18:51:37 +00:00
Matej Bačo 00b5236dea simplify diff 2025-12-29 18:41:35 +01:00
Damodar LohaniandGitHub c4fcec0a8a Merge pull request #11035 from appwrite/feat-storage-module
Feat: storage module
2025-12-29 19:34:15 +05:45
Damodar Lohani ca877fa71d Catch query parse exceptions 2025-12-29 13:24:24 +00:00
Damodar LohaniandGitHub 425dd9639c Merge branch '1.8.x' into feat-storage-module 2025-12-29 18:17:15 +05:45
Matej Bačo df57b59786 Merge branch '1.8.x' into chore-php-types 2025-12-29 13:24:50 +01:00
Matej Bačo eda189dbf1 AI review improvements 2025-12-29 13:24:26 +01:00
Chirag AggarwalandGitHub e60391bdb3 Merge pull request #11037 from appwrite/more-sdk-config 2025-12-29 17:45:11 +05:30
Chirag Aggarwal 57157a71b4 chore: more sdk config flexibility 2025-12-29 16:44:19 +05:30
Shmuel FogelandGitHub 8a52975048 Merge pull request #11036 from appwrite/functions-skip-subqueiry
Functions skip variables subquery
2025-12-29 13:08:00 +02:00
HemachandarandGitHub 45e3267930 Merge pull request #10985 from appwrite/ser-331
Support for sync-ing certificate generation status
2025-12-29 16:20:50 +05:30
Hemachandar 2b96d60c1e copilot - code quality 2025-12-29 16:01:19 +05:30
Matej Bačo ee911e3df6 Finalize unit key tests 2025-12-29 11:21:49 +01:00
fogelito 417bb22790 use Query::contains 2025-12-29 11:55:21 +02:00
Matej Bačo eb2c616089 Improve key unit tests 2025-12-29 10:47:27 +01:00
Hemachandar 1b70bc812b keep certificate renewal in maintenance worker 2025-12-29 14:51:59 +05:30
HemachandarandGitHub e7e429142f Merge branch '1.8.x' into ser-331 2025-12-29 14:20:20 +05:30
Damodar Lohani 80bb9a1f33 fixe endpoints 2025-12-29 08:41:38 +00:00
fogelito 774e3af61c skip variables subquery 2025-12-29 10:20:32 +02:00
Matej Bačo 5f5d9b4fcb Add async key cleanup 2025-12-29 09:04:47 +01:00
Matej Bačo 7c56a76feb self PR review fixes 2025-12-29 08:59:07 +01:00
Damodar Lohani 30373980f1 fix update endpoint 2025-12-29 07:56:46 +00:00
Damodar Lohani 8cf12f685a remove controller 2025-12-29 07:44:08 +00:00
Damodar Lohani f99cb20d05 Initialize storage module and remove storage and fix remaining endpoints 2025-12-29 07:44:03 +00:00
Damodar LohaniandGitHub a7724028b3 Merge branch '1.8.x' into feat-audits-upgrade 2025-12-29 13:23:20 +05:45
ArnabChatterjee20kandGitHub 7026052e04 Merge branch '1.8.x' into new-pool-adapter 2025-12-29 11:58:36 +05:30
Chirag AggarwalandGitHub a0514d2aac Merge pull request #11032 from appwrite/more-sdk-fixes 2025-12-29 11:52:04 +05:30
Chirag AggarwalandGitHub ac137cee3d Merge pull request #11027 from appwrite/lohanidamodar-patch-2 2025-12-29 11:51:00 +05:30
Damodar Lohani f4f4ad9c7d format 2025-12-29 01:51:07 +00:00
Damodar Lohani da7738edaa Feat: Storage module 2025-12-29 01:50:39 +00:00
shimon 6c1f967509 add functionsEvents and webhooksEvents 2025-12-28 18:10:44 +02:00
Chirag Aggarwal 58e091099f fix grammar 2025-12-28 21:17:43 +05:30
Chirag Aggarwal 6410a245eb fix: sdk configuration options 2025-12-28 20:59:46 +05:30
fogelito 670edd845e Merge branch '1.8.x' of https://github.com/appwrite/appwrite into lohanidamodar-patch-2 2025-12-28 17:23:10 +02:00
Chirag AggarwalandGitHub 834c18f9fa Merge pull request #11031 from appwrite/fix-default-namespace-in-sdks 2025-12-28 20:50:35 +05:30
Chirag Aggarwal 77930a0cec fix: default namespace in sdks 2025-12-28 20:46:23 +05:30
fogelito 5d9201466b functions + databases 2025-12-28 16:54:38 +02:00
fogelito d583b5e228 All again 2025-12-28 15:33:00 +02:00
fogelito c463a9a662 Only functions 2025-12-28 15:14:56 +02:00
fogelito bd9f02b689 Only functions 2025-12-28 15:01:29 +02:00
fogelito 472e2c282d skip specific filters 2025-12-28 14:31:46 +02:00
fogelito 7a89779827 revert 2025-12-28 14:17:48 +02:00
fogelito 126de78ce2 skip functions 2025-12-28 14:13:56 +02:00
fogelito e919896eef Merge remote-tracking branch 'origin/lohanidamodar-patch-2' into lohanidamodar-patch-2 2025-12-28 13:49:05 +02:00
fogelito fd3475f945 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into lohanidamodar-patch-2 2025-12-28 13:47:29 +02:00
Damodar Lohani 9c8f565211 use the helper method instead 2025-12-28 10:33:12 +00:00
Damodar Lohani 08f30224b6 Use disable instead of skip agin 2025-12-28 10:26:21 +00:00
Damodar LohaniandGitHub 6d02554f2a Merge pull request #11026 from appwrite/fix-file-permissions
Fix: Document/File set user permission only if not privileged user
2025-12-28 15:43:35 +05:45
Damodar Lohani 61c619f373 remove disables 2025-12-28 09:27:21 +00:00
Damodar LohaniandGitHub 419d56b4b1 Merge branch '1.8.x' into feat-audits-upgrade 2025-12-28 14:58:36 +05:45
Damodar Lohani b32dd316e1 Fix: use skip filters instead 2025-12-28 09:09:11 +00:00
Matej Bačo 04f660e44b Dedicate project test 2025-12-28 10:06:29 +01:00
Damodar Lohani d2cda9770b Use get audit resource for audit cleanup. 2025-12-28 09:01:37 +00:00
Damodar LohaniandGitHub 22042b4749 Merge branch '1.8.x' into fix-file-permissions 2025-12-28 14:23:31 +05:45
Damodar LohaniandGitHub aa17eeb6e3 Disable filters for platform and project databases - Stat resources
- Disable filters for platform and project databases - Stat resources
2025-12-28 14:19:28 +05:45
Shimon NewmanandGitHub 1529357821 Merge pull request #11022 from appwrite/fix-bulk-insert-webhook-validation
add webhook exist validation in bulkTrigger()
2025-12-28 09:31:13 +02:00
Damodar Lohani 3a98361736 add test 2025-12-28 02:02:34 +00:00
Damodar Lohani 0c425dbac3 Fix: assign user permission to files/documents only if not a previleged user 2025-12-28 01:48:23 +00:00
Matej Bačo b4c1b96d43 Fix General tests 2025-12-27 19:28:08 +01:00
Matej Bačo 44d8c66e95 Merge branch '1.8.x' into chore-php-types 2025-12-27 19:18:45 +01:00
Matej Bačo 6774de4eef add todo 2025-12-27 19:18:25 +01:00
Matej Bačo ee0f15eed6 QA bug fixing 2025-12-27 19:08:12 +01:00
Matej Bačo c54d1d29a5 Update stats of all key ypes 2025-12-27 18:44:01 +01:00
Chirag AggarwalandGitHub 69b861cd87 Merge pull request #11025 from appwrite/fix-specs-generation 2025-12-27 21:45:56 +05:30
Chirag Aggarwal 0599d23cc5 fix: specs generation getPlatforms method 2025-12-27 20:48:33 +05:30
Chirag AggarwalandGitHub a37b4a40d9 Merge pull request #11024 from appwrite/turnoff-warning-logs 2025-12-27 20:21:07 +05:30
Chirag Aggarwal 7e37046959 chore: remove warning logs when skipping ssl certificate generation 2025-12-27 20:19:27 +05:30
shimon f9efdfd98e fix: use nullsafe operator for project attribute retrieval in Action class 2025-12-25 19:07:51 +02:00
shimon d53090647b Merge branch '1.8.x' of github.com:appwrite/appwrite into fix-bulk-insert-webhook-validation 2025-12-25 18:56:30 +02:00
shimon e8b8a8fe52 add webhook exist validation in balkTrigger() 2025-12-25 18:52:57 +02:00
ArnabChatterjee20k 3b4196735a refactor: simplify query handling in Realtime adapter and enhance error messaging for unsupported queries 2025-12-24 21:02:03 +05:30
ArnabChatterjee20k 7e315f79cc refactor: improve query handling in Realtime adapter and update RuntimeQuery filter logic 2025-12-24 20:50:05 +05:30
ArnabChatterjee20k 336bd44826 fixed payload in adapter 2025-12-24 20:10:00 +05:30
ArnabChatterjee20k 881d96a653 linting 2025-12-24 19:06:34 +05:30
ArnabChatterjee20k 874e5f61f0 Merge remote-tracking branch 'upstream/1.8.x' into dat-969 2025-12-24 19:01:13 +05:30
ArnabChatterjee20k 39cf207df9 re 2025-12-24 18:57:58 +05:30
ArnabChatterjee20k 11f23d99fb Revert "temp-check: force purging cache"
This reverts commit 00e00ff166.
2025-12-24 17:09:01 +05:30
ArnabChatterjee20k 00e00ff166 temp-check: force purging cache 2025-12-24 16:55:13 +05:30
ArnabChatterjee20kandGitHub 57c80d5967 Merge branch '1.8.x' into new-pool-adapter 2025-12-24 16:48:25 +05:30
ArnabChatterjee20k f36dd2748c fixed tests 2025-12-24 16:22:02 +05:30
Damodar Lohani 69430154e6 update autit to rc version 2025-12-24 10:24:51 +00:00
Damodar LohaniandGitHub 0601305545 Merge branch '1.8.x' into feat-audits-upgrade 2025-12-24 15:21:49 +05:45
Chirag AggarwalandGitHub 73d3d15ed8 Merge pull request #11017 from appwrite/fix-getscreenshot-use-image-format 2025-12-24 14:42:54 +05:30
ArnabChatterjee20k 2baa33351d fix tests 2025-12-24 14:38:43 +05:30
ArnabChatterjee20k f7668e0411 * added concurrent traffic load tests for realtime
* added assert eventual case for project console
* linting
2025-12-24 13:41:31 +05:30
Chirag Aggarwal b78be65376 fix: make getScreenshot output param to use ImageFormat enum 2025-12-24 13:39:51 +05:30
Damodar Lohani 5519086c29 Use db 3.x audit 2025-12-24 01:30:02 +00:00
Damodar Lohani 33b9f33f44 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into feat-audits-upgrade 2025-12-24 01:26:49 +00:00
Luke B. SilverandGitHub 0e75793bef Merge pull request #11013 from appwrite/fix-tests-jwt
Fix JWT test
2025-12-23 17:23:36 +00:00
Matej Bačo 2f4a362e1b Merge branch '1.8.x' into fix-tests-jwt 2025-12-23 16:31:32 +01:00
Matej Bačo cda843d8f6 Fix JWT test 2025-12-23 16:31:17 +01:00
ArnabChatterjee20k 62e881b7e9 reverted the use retry mechanism of the pools 2025-12-23 20:34:59 +05:30
Matej BačoandGitHub d6a40a35ee Merge pull request #11012 from appwrite/chore-increase-jwt-abuse
Increase JWT abuse limit
2025-12-23 15:48:00 +01:00
Matej Bačo e111b4cc18 Increase JWT abuse limit 2025-12-23 15:42:14 +01:00
Matej Bačo c08acedf6a Fix key test 2025-12-23 15:12:41 +01:00
ArnabChatterjee20k c03cd258ce changed the pool size distribution 2025-12-23 18:30:53 +05:30
Matej Bačo 9477a5d980 Fix extensability of collections 2025-12-23 13:30:43 +01:00
ArnabChatterjee20k 7e4d404549 updated composer json 2025-12-23 17:38:52 +05:30
Matej Bačo c0c1d693c2 DB schema update for keys 2025-12-23 13:06:25 +01:00
Matej Bačo 6e47fb6c70 Implement auth for organization and account keys 2025-12-23 13:06:19 +01:00
ArnabChatterjee20k 8df22db3eb added swoole based pools for realtime 2025-12-23 17:33:34 +05:30
Chirag AggarwalandGitHub cdbd80ceda Merge pull request #11010 from appwrite/update-schedule-functions 2025-12-23 17:26:09 +05:30
Hemachandar 58751bbdf1 lint 2025-12-23 16:49:35 +05:30
Matej Bačo cca49f8f6a Improve docs 2025-12-23 12:11:01 +01:00
Matej Bačo b0bd9e5b78 Fix security requirement 2025-12-23 12:09:07 +01:00
HemachandarandGitHub 21334ecdff Merge branch '1.8.x' into ser-331 2025-12-23 16:37:22 +05:30
Matej Bačo 6f7cca7d68 Allow key header for account keys 2025-12-23 12:06:30 +01:00
Hemachandar 47ea2893c3 rename task to Interval 2025-12-23 16:32:43 +05:30
Matej Bačo bce3ce85d5 account key support 2025-12-23 11:59:38 +01:00
Chirag Aggarwal bbeca28026 refactor use env variables for queue and class names 2025-12-23 16:09:42 +05:30
Chirag Aggarwal 9ab9f6d709 chore: update schedule functions task 2025-12-23 15:06:29 +05:30
Chirag AggarwalandGitHub 309fde38f7 Merge pull request #11001 from appwrite/update-sdks-console 2025-12-23 13:48:02 +05:30
Chirag Aggarwal 3310d271bf fix import 2025-12-23 12:35:48 +05:30
Chirag Aggarwal 4e12b6236e Merge branch '1.8.x' into update-sdks-console 2025-12-23 12:34:23 +05:30
Torsten DittmannandGitHub 4df8157d74 Merge pull request #11009 from appwrite/feat-allow-custom-jwt-duration 2025-12-22 21:50:45 +04:00
Torsten Dittmann 37e76e52f6 Make JWT expiry configurable for account JWTs 2025-12-22 21:27:29 +04:00
Torsten Dittmann cd824faf64 Add test for JWT with custom duration 2025-12-22 21:19:50 +04:00
Torsten Dittmann 96b11b02e6 feat: allow custom jwt duration 2025-12-22 21:08:48 +04:00
ArnabChatterjee20k b7e2606b9f Enhance Realtime functionality with query support and improve tests
- Updated Realtime adapter to handle queries during subscription.
- Added query filtering capabilities in RuntimeQuery class.
- Modified RealtimeBase and RealtimeCustomClientTest to support query parameters in WebSocket connections.
- Improved test coverage for account and database channels with queries.
2025-12-22 18:01:00 +05:30
Matej Bačo 09d71e73af keys list to be public as its reused for more types of keys 2025-12-22 11:37:38 +01:00
Matej Bačo 465912822f Mark reused key response public 2025-12-22 11:32:45 +01:00
Chirag Aggarwal 9f24cb4aa3 trigger ci 2025-12-22 12:48:23 +05:30
Chirag Aggarwal 9e4e23ad3f chore: update sdks script console log 2025-12-21 22:03:30 +05:30
Matej Bačo ca43281fa9 Simplify PR 2025-12-20 09:20:39 +01:00
Matej Bačo c69382f29f Fix invalid index 2025-12-19 16:11:43 +01:00
Matej Bačo d45d27b780 Merge branch '1.8.x' into chore-php-types 2025-12-19 16:06:46 +01:00
Jake BarnbyandGitHub 3e96f9c2f1 Merge pull request #10990 from appwrite/project-queries
Allow queries on projects xlist
2025-12-19 14:39:47 +00:00
Jake BarnbyandGitHub dd9f6ed820 Merge pull request #10996 from appwrite/revert-10872-refactor-auth-single-instance
Revert "Refactor auth single instance"
2025-12-19 14:37:41 +00:00
Darshan f7f1bc8352 Merge remote-tracking branch 'origin/project-queries' into project-queries 2025-12-19 19:36:17 +05:30
Darshan bf50fbeb6c bump: order for easier review. 2025-12-19 19:36:05 +05:30
DarshanandGitHub 1f7bfb3352 Merge branch '1.8.x' into project-queries 2025-12-19 19:33:22 +05:30
Darshan 406181e887 revert: to method call 2025-12-19 19:31:05 +05:30
Jake BarnbyandGitHub a7b243aff9 Merge pull request #10992 from appwrite/update-header-name
Update: Header name.
2025-12-19 13:56:00 +00:00
Jake BarnbyandGitHub dc85d44647 Revert "Refactor auth single instance" 2025-12-20 02:54:50 +13:00
Jake BarnbyandGitHub 71621f4ccf Merge pull request #10995 from appwrite/revert-10987-fix-auth
Revert "Fix auth calls"
2025-12-19 13:54:38 +00:00
Jake BarnbyandGitHub 68deee4a56 Revert "Fix auth calls" 2025-12-20 02:54:21 +13:00
Matej BačoandGitHub a20392d13a Merge pull request #10993 from appwrite/feat-agents-md
Feat: Create AGENTS.md
2025-12-19 14:11:14 +01:00
Matej BačoandGitHub 859d146e85 Apply suggestions from code review 2025-12-19 13:41:18 +01:00
Matej Bačo 69d5ce0f55 Switch over to resource-based key DB structure 2025-12-19 13:40:32 +01:00
Darshan e43292052c ci: empty commit 2025-12-19 18:02:07 +05:30
Matej Bačo ee7103c4b0 Typo fix 2025-12-19 13:20:57 +01:00
Matej Bačo 1b1ef80b10 Create AGENTS.md 2025-12-19 13:09:48 +01:00
Matej Bačo 0a3877a900 New DB schema 2025-12-19 13:09:34 +01:00
Darshan 33f90fcf6a re-add headers. 2025-12-19 17:02:55 +05:30
Matej Bačo 31b9ff4f05 Merge branch '1.8.x' into chore-php-types 2025-12-19 12:26:51 +01:00
Matej Bačo 1c3f778da9 PR review fix 2025-12-19 12:26:15 +01:00
Darshan 2f1eee390b update: header name. 2025-12-19 16:23:22 +05:30
Darshan 35bf13cfab update: variable name :D 2025-12-19 16:19:15 +05:30
Darshan f814fd980b update: address comments. 2025-12-19 16:16:56 +05:30
Darshan 3ddfd9c3b0 update: address comment. 2025-12-19 15:52:19 +05:30
DarshanandGitHub 75d8110ad9 Merge branch '1.8.x' into project-queries 2025-12-19 14:23:25 +05:30
Darshan c958f3f1cb update: allow queries on projects xlist. 2025-12-19 14:13:52 +05:30
Steven NguyenandGitHub 06e3a144a3 Merge pull request #10939 from appwrite/fix-smtp-auth-check
Fix smtp auth check
2025-12-18 17:55:43 -08:00
Steven Nguyen 36519c13a8 Merge branch '1.8.x' into fix-smtp-auth-check 2025-12-18 17:21:03 -08:00
Steven NguyenandGitHub 44ec8de6bf Merge pull request #10946 from appwrite/1.8.x
Sync 1.8.x into main
2025-12-18 17:06:43 -08:00
Matej BačoandGitHub 9fb1aaadaf Merge pull request #10984 from appwrite/fix-devkey-scopes
Fix: Add scopes for devkeys
2025-12-18 17:26:15 +01:00
Torsten DittmannandGitHub d9ac4f2f2d Merge pull request #10986 from appwrite/fix-oauth-verified-emails 2025-12-18 19:04:56 +04:00
Matej Bačo 6f16b56f31 Allow Key extensions 2025-12-18 15:55:11 +01:00
e61f3c7a42 Update phpunit.xml
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-18 18:03:02 +04:00
Jake BarnbyandGitHub 1b6d51a116 Merge pull request #10987 from appwrite/fix-auth
Fix auth calls
2025-12-18 12:56:58 +00:00
Jake Barnby 0058e88217 Fix auth calls 2025-12-19 01:53:05 +13:00
Torsten Dittmann 81b4065921 Fix identity connecting
- Add MockUnverified OAuth2 provider config
- Add /v1/mock/tests/general/oauth2/user-unverified endpoint
- Add MockUnverified class for unverified OAuth2 flow
- Update Mock::isEmailVerified to respect user['verified'] flag
- Add end-to-end tests for linking unverified and verified OAuth2 users
- Enable stopOnFailure in phpunit.xml
2025-12-18 16:44:04 +04:00
Matej Bačo 59f178d634 Improve PHP types for extensability 2025-12-18 13:37:50 +01:00
Torsten Dittmann d094d6a081 Remove OAuth email verification comments 2025-12-18 16:00:41 +04:00
Hemachandar cf90653deb lint 2025-12-18 17:30:08 +05:30
Torsten Dittmann bae194e866 Link account by email during OAuth 2025-12-18 15:59:33 +04:00
Torsten Dittmann 144e88452e Use general bad request for unverified OAuth email 2025-12-18 15:49:59 +04:00
Torsten Dittmann 89980b1f0e Enforce email verification when linking OAuth2 2025-12-18 15:46:03 +04:00
Hemachandar 16fb25ce5a tiny 2025-12-18 17:10:02 +05:30
Matej Bačo 7dea3f19a9 Add scopes for devkeys 2025-12-18 12:31:00 +01:00
Hemachandar e78887001a spacing 2025-12-18 16:18:23 +05:30
Hemachandar aa4ecdf138 emit events 2025-12-18 16:09:48 +05:30
Jake BarnbyandGitHub 9bda83104d Merge pull request #10983 from appwrite/chore-specs
Chore specs
2025-12-18 09:53:31 +00:00
Hemachandar 3783378288 add action to payload 2025-12-18 15:14:30 +05:30
Jake BarnbyandGitHub 937cac454a Merge branch '1.8.x' into chore-specs 2025-12-18 09:32:37 +00:00
Jake BarnbyandGitHub 36eb28dc22 Merge pull request #10872 from appwrite/refactor-auth-single-instance
Refactor auth single instance
2025-12-18 09:05:14 +00:00
Hemachandar 5aff78a619 Merge branch '1.8.x' into ser-331 2025-12-18 14:33:34 +05:30
Jake Barnby e6781cb4c3 Release SDKs 2025-12-18 21:41:50 +13:00
Hemachandar 62b391ef68 certificate status exception 2025-12-18 13:46:56 +05:30
Hemachandar 6f1d2d094a rename task 2025-12-18 13:00:49 +05:30
Jake Barnby 36690229c5 Update lock 2025-12-18 20:23:49 +13:00
Jake Barnby fe5d9cc662 Update changelog 2025-12-18 20:23:42 +13:00
Jake Barnby f46ec96040 Fix merge 2025-12-18 20:11:52 +13:00
Hemachandar 88bd35ce98 periodic task for rule verification & certificate generation 2025-12-18 12:40:24 +05:30
Jake Barnby 2dc813342a Fix merge 2025-12-18 19:30:43 +13:00
Jake Barnby fd08f7dd36 Update lock 2025-12-18 18:43:07 +13:00
Jake Barnby 13f4aa9a80 Merge remote-tracking branch 'origin/1.8.x' into refactor-auth-single-instance
# Conflicts:
#	app/controllers/api/account.php
#	app/controllers/api/vcs.php
#	app/controllers/general.php
#	app/controllers/shared/api.php
#	app/init/resources.php
#	composer.lock
#	src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
#	src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
#	src/Appwrite/Platform/Workers/Migrations.php
2025-12-18 18:17:41 +13:00
Luke B. SilverandGitHub 22fba9defc Merge pull request #10979 from appwrite/fix-credentials
Fix entrypoint
2025-12-17 19:38:36 +00:00
fogelito fcbb073bcc endpoint revert 2025-12-17 20:53:22 +02:00
Hemachandar db0dbeb27b simplify 2025-12-17 21:04:15 +05:30
Hemachandar 0e27088e2a change function signature 2025-12-17 20:54:18 +05:30
Hemachandar 70a7deaa38 Refactor certificate generation in worker 2025-12-17 20:46:54 +05:30
Luke B. SilverandGitHub 514934649e Merge pull request #10977 from appwrite/fix-sync-updated-schedules
Fix sync updated schedules
2025-12-17 14:57:39 +00:00
loks0n ee8e7333d8 fix: sync updated schedules 2025-12-17 14:19:04 +00:00
DarshanandGitHub 8d94b6585e Merge pull request #10976 from appwrite/platforms-in-migrations 2025-12-17 18:14:25 +05:30
DarshanandGitHub 9fd92ca5ac Merge pull request #10974 from appwrite/update-template 2025-12-17 18:02:31 +05:30
Darshan 6d9a6051bf add: platform support on migrations. 2025-12-17 17:41:14 +05:30
Darshan 461c5895a4 fix: missing cases. 2025-12-17 17:27:18 +05:30
DarshanandGitHub 94fc0416e6 Merge branch '1.8.x' into update-template 2025-12-17 17:08:16 +05:30
Darshan ad54a8c334 fix: missing cases. 2025-12-17 17:05:08 +05:30
Chirag AggarwalandGitHub e2b72e1f58 Merge pull request #10975 from appwrite/sync-sdks 2025-12-17 16:57:14 +05:30
Darshan e1a9eb4994 fix: missed injections. 2025-12-17 16:04:29 +05:30
Darshan b0c3ab71c4 add: header in tests. 2025-12-17 16:02:06 +05:30
Chirag Aggarwal ce8f72bee9 data examples update 2025-12-17 15:58:04 +05:30
Darshan ee4f3e2df4 update: project name on MFA. 2025-12-17 15:54:49 +05:30
Darshan 930db2bcc4 add: platform injection on MFA endpoint as well. 2025-12-17 15:50:34 +05:30
Chirag Aggarwal fadcb39765 chore: sync sdk examples after spec changes 2025-12-17 15:48:49 +05:30
Darshan 5034597750 add: sender name as well. 2025-12-17 15:43:10 +05:30
Darshan f73b48d4ca add: profile mode for console sdk. 2025-12-17 15:19:07 +05:30
Darshan 0a7f31333d fix: key, lol. 2025-12-17 14:46:24 +05:30
Darshan 8964fbc821 fix: projectName. 2025-12-17 14:33:49 +05:30
Damodar Lohani 8baa5cb659 Merge remote-tracking branch 'origin/1.8.x' into feat-audits-upgrade 2025-12-17 08:59:05 +00:00
Darshan 2e5bea1aee update: email template to be platform agnostic. 2025-12-17 14:26:45 +05:30
Jake BarnbyandGitHub 759792c447 Merge pull request #10972 from appwrite/max-query-values-worker
Max query values worker
2025-12-17 08:05:08 +00:00
fogelito 86deab8f6c increase APP_DATABASE_QUERY_MAX_VALUES_WORKER 2025-12-17 09:32:59 +02:00
Jake Barnby 3cf8f151c4 Update versions 2025-12-17 19:19:05 +13:00
Jake Barnby 4e0945d0ed Update specs 2025-12-17 19:18:57 +13:00
Luke B. SilverandGitHub 052c8d4ead Merge pull request #10971 from appwrite/fix-fallback-platfor
fix: fallback platform
2025-12-16 23:23:03 +00:00
loks0n 3c3aa6d416 fix: fallback platform 2025-12-16 22:56:11 +00:00
Luke B. SilverandGitHub eabe06d981 Merge pull request #10969 from appwrite/fix-scheduler-base
Scheduler base patch chunks
2025-12-16 15:36:54 +00:00
fogelito 8d4c6a8218 patch $batchSize 2025-12-16 17:17:03 +02:00
Luke B. SilverandGitHub 1a09913a27 Merge pull request #10967 from appwrite/fix-oauth-custom-domains
fix: oauth custom domains
2025-12-16 14:31:31 +00:00
loks0n e8e47746ea fix: oauth custom domains 2025-12-16 14:14:57 +00:00
Matej BačoandGitHub 546cd6596a Merge pull request #10966 from appwrite/fix-empty-template
Fix CSV template build flow (empty source)
2025-12-16 13:39:32 +01:00
HemachandarandGitHub 58be8f411a Merge pull request #10962 from appwrite/ser-721
Improve rule verification flow & add logs
2025-12-16 16:43:14 +05:30
Hemachandar 990209b624 feedback 2025-12-16 16:17:15 +05:30
Matej Bačo 850a834211 Fix CSV template build flow (empty source) 2025-12-16 11:23:44 +01:00
Jake BarnbyandGitHub dfccbd0e16 Merge pull request #10965 from appwrite/fix-deletes-bug
Fix: deletes bug
2025-12-16 09:57:26 +00:00
Jake BarnbyandGitHub cda5b73600 Merge pull request #10964 from appwrite/1.8.x-bump-v1
Bump 1.8.x
2025-12-16 09:57:02 +00:00
Matej Bačo 1f4534d674 Fix deletes bug 2025-12-16 10:44:02 +01:00
Luke B. SilverandGitHub 9ba970b3c1 Merge pull request #10950 from appwrite/refactor-specs-task-methods
Refactor Specs task for better code organization
2025-12-16 09:40:55 +00:00
fogelito ef54603367 bump 2025-12-16 11:35:34 +02:00
Jake BarnbyandGitHub 2c855427aa Merge pull request #10963 from appwrite/worker-error-add-previous
Worker error add previous errors
2025-12-16 09:24:16 +00:00
Jake BarnbyandGitHub 9b62d341d2 Merge pull request #10955 from appwrite/migration-cleanup
Migration cleanup
2025-12-16 09:23:18 +00:00
fogelito d3e0050739 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into worker-error-add-previous 2025-12-16 10:44:41 +02:00
fogelito af8a27c559 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into migration-cleanup 2025-12-16 10:42:12 +02:00
Jake BarnbyandGitHub b986cd806c Merge pull request #10913 from appwrite/refactor-functions-schedule
Refactor schedule base
2025-12-16 08:33:09 +00:00
fogelito 5ba7501842 revert previous errors 2025-12-16 10:30:31 +02:00
fogelito 421696a26e Previous errors 2025-12-16 10:27:28 +02:00
fogelito c6214ef4a8 Camel case 2025-12-16 10:23:05 +02:00
fogelito b0b9c412fc Add previous messages 2025-12-16 10:13:08 +02:00
fogelito 1c165b8c8d endpoint fallback 2025-12-16 09:49:35 +02:00
fogelito 6fbacb484b Merge branch '1.8.x' of https://github.com/appwrite/appwrite into migration-cleanup
# Conflicts:
#	src/Appwrite/Platform/Workers/Migrations.php
2025-12-16 09:31:58 +02:00
fogelito 91a3ee0c9d Add File info 2025-12-16 09:24:47 +02:00
HemachandarandGitHub 49b1c3db79 Merge branch '1.8.x' into ser-721 2025-12-16 12:41:44 +05:30
Hemachandar dfb53895eb tiny 2025-12-16 12:40:42 +05:30
Jake Barnby 7cdb7af07c Remove early return to ensure cycle timestamp is updated 2025-12-16 20:06:38 +13:00
Hemachandar 109967953e feedback + tests 2025-12-16 12:20:59 +05:30
Jake BarnbyandGitHub e3e7f56bb8 Merge pull request #10961 from appwrite/refactor-messaging-and-queue
chore: lazy init sms adapter + late static binding
2025-12-16 06:25:15 +00:00
Jake BarnbyandGitHub 80057383e1 Merge pull request #10952 from appwrite/issue-tableid
Tableid and Collectionid in increment and decrement routes
2025-12-16 06:05:11 +00:00
Jake Barnby c7fda6cc3f Merge remote-tracking branch 'origin/1.8.x' into refactor-functions-schedule
# Conflicts:
#	composer.lock
2025-12-16 18:35:40 +13:00
Hemachandar 5ae148f026 Add logs to rules 2025-12-16 10:59:16 +05:30
Chirag AggarwalandGitHub c744354973 Merge branch '1.8.x' into refactor-specs-task-methods 2025-12-16 10:51:28 +05:30
Chirag AggarwalandGitHub ddb0d5db26 Merge branch '1.8.x' into fix-smtp-auth-check 2025-12-16 10:51:01 +05:30
Chirag Aggarwal c7fd31285d chore: lazy init sms adapter + late static binding 2025-12-16 10:46:40 +05:30
Luke B. SilverandGitHub 8cd000c42d Merge pull request #10958 from appwrite/revert-backups-endpoints
Revert backups endpoints
2025-12-15 18:16:34 +00:00
loks0n e931938b78 revert: backups endpoints 2025-12-15 18:01:30 +00:00
loks0n 4b58e1a85e revert: backups endpoints 2025-12-15 16:55:48 +00:00
Chirag Aggarwal c6b6b7cdac fix: edit field 2025-12-15 21:41:17 +05:30
Luke B. SilverandGitHub 2a61cacdf9 Merge pull request #10957 from appwrite/fix-platform-defaults
fix: platform defaults
2025-12-15 15:17:05 +00:00
Levi van NoortandGitHub f4bca74a59 Merge pull request #10941 from appwrite/CLO-3704-utopia-request-extend
feat: improved reference client ip through _APP_TRUSTED_HEADERS
2025-12-15 16:15:18 +01:00
loks0n 7e6846f8f8 fix: platform defaults 2025-12-15 15:01:26 +00:00
Levi van Noort 03a19cc83c chore: set _APP_TRUSTED_HEADERS to x-forwarded-for 2025-12-15 15:49:21 +01:00
Levi van Noort 8a35762843 chore: update utopia-php/swoole version constraint to 0.8.* 2025-12-15 15:36:24 +01:00
Levi van NoortandGitHub 3869269e0c Merge branch '1.8.x' into CLO-3704-utopia-request-extend 2025-12-15 15:32:51 +01:00
Levi van Noort 0d7eb88a45 chore: update utopia-php/swoole to version 0.8.5 2025-12-15 15:08:23 +01:00
Levi van Noort cd90974e15 chore: changes to test to validate default behaviour instead 2025-12-15 13:22:29 +01:00
fogelito 54ba59da64 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into migration-cleanup 2025-12-15 09:43:06 +02:00
fogelito f837e6be03 cleanups 2025-12-15 09:42:26 +02:00
Damodar LohaniandGitHub cec5cd7bdc Merge branch '1.8.x' into feat-audits-upgrade 2025-12-15 09:44:52 +05:45
Damodar Lohani 63c2d72b2e Fix logs 2025-12-15 03:58:00 +00:00
Jake BarnbyandGitHub e9d9aa653f Merge pull request #10956 from appwrite/fix-cors-wildcard
fix: cors wildcard
2025-12-15 03:54:55 +00:00
Damodar Lohani 801219374c Fix queries 2025-12-15 02:50:21 +00:00
Damodar Lohani dc3459edd0 remove dump 2025-12-15 02:43:27 +00:00
Damodar Lohani ace9d86744 fix attribute 2025-12-15 02:41:56 +00:00
Damodar Lohani 2d3e34fd5a fix namespace 2025-12-15 02:20:10 +00:00
Damodar Lohani 69ad4ae930 Fix format 2025-12-15 02:15:45 +00:00
Damodar Lohani 0037305e16 Fix: failing tests 2025-12-15 02:12:35 +00:00
loks0n da0b2a7517 fix: cors wildcard 2025-12-14 22:41:15 +00:00
fogelito 6aafe2dd12 cleanup 2025-12-14 19:41:33 +02:00
Damodar Lohani 1a205c3f3b upgrade audit 2025-12-14 08:50:39 +00:00
Damodar Lohani 99966b5a24 upgrade audit 2025-12-14 08:35:40 +00:00
Damodar Lohani 2437d2be77 Upgrade audit and fix 2025-12-14 08:30:22 +00:00
Damodar Lohani 2043470bd0 fix typo 2025-12-14 08:00:31 +00:00
Damodar Lohani 667285d536 fix tyupe 2025-12-14 07:58:16 +00:00
Damodar Lohani 1199c1fc52 fix typo 2025-12-14 07:57:37 +00:00
Damodar Lohani f270e47b48 more fixes to audit queries 2025-12-14 07:50:21 +00:00
Darshan 5678860d13 add: optional support for assistant. 2025-12-14 13:07:26 +05:30
Damodar Lohani b83125a41e Fix audits creation 2025-12-14 07:22:39 +00:00
Chirag Aggarwal 5daacb7161 Refactor Specs task for better code organization
- Extract platform, auth counts, and keys into separate methods
- Add getPlatforms(), getAuthCounts(), and getKeys() methods
- Extract SDK platform logic into getSDKPlatformsForRouteSecurity()
- Add PHPDoc comments with proper type hints
- Move constructor to top of class for better readability
2025-12-14 11:21:44 +05:30
Damodar Lohani 2dcb131778 fix collection name 2025-12-14 02:34:33 +00:00
Damodar Lohani 30083598c6 fix audit 2025-12-14 02:33:17 +00:00
Damodar Lohani a0599d2658 Upgrade audit 2025-12-14 02:31:24 +00:00
Damodar Lohani c50db111d6 format 2025-12-14 02:19:53 +00:00
Damodar Lohani 9441295510 Feat: Audits upgrade 2025-12-14 01:43:35 +00:00
ArnabChatterjee20k e4e6c84362 linting 2025-12-13 23:45:57 +05:30
ArnabChatterjee20k 70395cfacc added tableid and collectionid in increment and decrement 2025-12-13 23:43:51 +05:30
DarshanandGitHub 7d2a95c573 Merge pull request #10947 from appwrite/optimize-dockerfile 2025-12-13 19:20:46 +05:30
Darshan f3f3ac80f8 fix: tests. 2025-12-13 18:59:12 +05:30
Darshan 45d5f14b83 add: targets. 2025-12-13 16:43:19 +05:30
Levi van Noort ce9a54fd4b chore: change implementation for trustedip 2025-12-12 15:31:42 +01:00
DarshanandGitHub 8e434b710b Merge pull request #10945 from appwrite/allow-custom-sender 2025-12-12 18:32:37 +05:30
Darshan 37b43b0ce0 address comment. 2025-12-12 17:49:48 +05:30
Darshan 2052496635 add: support for replyTo* and simplify variables. 2025-12-12 17:41:00 +05:30
Darshan 1cc6c9c0f1 Merge branch '1.8.x' into allow-custom-sender 2025-12-12 17:29:45 +05:30
Luke B. SilverandGitHub bbc76ffe37 Merge pull request #10943 from appwrite/fix-preflight-requests
fix preflight requests
2025-12-12 10:11:53 +00:00
Levi van Noort 6589c6a786 chore: bump utopia-php/framework to version 0.33.35 2025-12-12 10:54:09 +01:00
loks0n 2cb86c57ef fix: preflight requests 2025-12-12 09:49:04 +00:00
Levi van Noort 90fb5b6321 feat: improved reference client ip through _APP_TRUSTED_HEADERS 2025-12-12 08:42:49 +01:00
Steven NguyenandGitHub d4517384dc fix: update project tests to reflect changes in project count and names 2025-12-12 07:16:49 +00:00
Darshan 6787b0c7f1 Merge branch '1.8.x' into allow-custom-sender 2025-12-12 12:17:45 +05:30
Steven NguyenandGitHub a94ccdb4f5 fix: enhance SMTP authentication by using env vars 2025-12-12 06:02:17 +00:00
Steven NguyenandGitHub 9518764cae Merge branch '1.8.x' into fix-smtp-auth-check 2025-12-12 05:24:33 +00:00
Steven NguyenandGitHub d174233cd6 fix: update SMTP configuration and enhance validation checks 2025-12-12 04:58:59 +00:00
Steven NguyenandGitHub 278679ab10 Merge pull request #10938 from appwrite/chore-sync-main
Sync main into 1.8.x
2025-12-11 20:06:52 -08:00
Steven NguyenandGitHub 72ae883587 Merge branch 'main' into chore-sync-main 2025-12-11 22:40:23 +00:00
Steven NguyenandGitHub 3d0aa74ffe Merge pull request #10863 from VijaykumarPujar-tech/new_branch
Fix: Implement mandatory authentication check for Custom SMTP configuration
2025-12-11 13:51:27 -08:00
Luke B. SilverandGitHub e4a7b1b103 Merge pull request #10937 from appwrite/fix-imagine-hostnames-2
Fix imagine hostnames 2
2025-12-11 19:05:12 +00:00
loks0n c6ffa181a2 fix: imagine hostnames 2 2025-12-11 18:53:58 +00:00
Matej BačoandGitHub cd8e93db60 Merge pull request #10031 from appwrite/chore-add-new-site-templates
Add formspree and react-admin templates to sites
2025-12-11 16:56:03 +01:00
Aditya Oberai ceb2108012 shift db seeding command to build command 2025-12-11 15:00:59 +00:00
Aditya Oberai 383f0c1ef1 remove unnecessary images 2025-12-11 15:00:04 +00:00
Matej Bačo 52ccbfcdf7 AI code quality fixes 2025-12-11 15:52:44 +01:00
Steven NguyenandGitHub 35fe622548 Merge pull request #10925 from Ujjwaljain16/fix-10740-mfa-recovery-code-validation
fix: resolve MFA recovery code validation in 1.8.0
2025-12-11 06:28:06 -08:00
Aditya OberaiandGitHub d502ab0cd5 Merge branch '1.8.x' into chore-add-new-site-templates 2025-12-11 18:35:52 +05:30
DarshanandGitHub 72f86ce5ef Merge pull request #10934 from appwrite/database-errors 2025-12-11 16:58:35 +05:30
DarshanandGitHub 062233cdbc Merge branch '1.8.x' into database-errors 2025-12-11 16:10:00 +05:30
Darshan 102d3fd245 fix: tests. 2025-12-11 16:08:13 +05:30
Darshan cb9705f0a2 update: better signature. 2025-12-11 15:59:24 +05:30
Luke B. SilverandGitHub 61d1546d14 Merge pull request #10911 from appwrite/feat-multiple-app-domains
feat: multiple app domains
2025-12-11 10:09:33 +00:00
DarshanandGitHub 2e4ac091d1 Merge branch '1.8.x' into database-errors 2025-12-11 15:27:42 +05:30
Darshan ef5f0ed6b4 update: to use placeholders. 2025-12-11 15:26:45 +05:30
22a1a5a452 Update app/controllers/general.php
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-11 09:41:31 +00:00
Matej Bačo e738bae523 Fix screenshot script; auto-fill API key, custom required env vars
Tag is correct fallback: https://github.com/appwrite/appwrite/blob/decf2c7a1bc5d2d989434fb0adc6a6fb44a77db2/src/Appwrite/Utopia/Request/Filters/V21.php#L28
2025-12-11 10:31:05 +01:00
loks0n f765eb92e7 feat: cors service 2025-12-11 09:01:31 +00:00
loks0n 50c6a35079 feat: multiple app domains 2025-12-11 09:01:31 +00:00
Chirag AggarwalandGitHub f8c39c8f06 Merge pull request #10933 from appwrite/hide-deprecated-methods-from-docs 2025-12-11 14:23:21 +05:30
Chirag Aggarwal aa12ef6569 Add public visibility control for deprecated methods
- Add 'public' parameter to SDK Method class to control documentation visibility
- Mark deprecated methods as non-public (public: false) to hide them from docs
- Update OpenAPI and Swagger specs to reflect visibility changes
- Affected endpoints: account verification, messaging providers, and MFA authenticators
2025-12-11 13:53:34 +05:30
Steven NguyenandGitHub 5912a4c4a0 Merge pull request #10931 from appwrite/main
Sync main into 1.8.x
2025-12-10 22:50:12 -08:00
Damodar LohaniandGitHub e85079a42f Merge pull request #10928 from appwrite/fix-remove-temp-fix
Fix: remove temp queries
2025-12-11 04:58:41 +05:45
Damodar Lohani 4c9a94f5e2 Fix format 2025-12-10 22:48:07 +00:00
Ujjwaljain16 7fc8c6ae43 Simplify testMFARecoveryCodeChallenge to use existing session 2025-12-11 01:50:52 +05:30
Damodar Lohani 61067bd8d3 Fix: remove temp queries 2025-12-10 10:32:51 +00:00
Ujjwaljain16 fb3b3ae51e fix: add missing empty body array parameter to recovery codes API call 2025-12-10 15:40:40 +05:30
Darshan 39aa4c4a9a add: setfrom. 2025-12-10 15:14:55 +05:30
Darshan b03aa8d632 add: custom senders for cloud. 2025-12-10 14:32:04 +05:30
Ujjwaljain16 0215577bc7 fix: MFA recovery code validation and test status code
- Fixed HTTP status code: POST /v1/account/mfa/recovery-codes now returns 201 (CREATED) instead of 200
- Updated testMFARecoveryCodeChallenge to expect 201 status code
- Added array_merge with origin header to all API calls in test for proper CORS validation
- Removed trailing whitespace for PSR-12 compliance

Fixes #10740
2025-12-10 05:37:43 +05:30
Ujjwaljain16 768d293cf5 reuse session from testCreateAccountSession instead of creating new session
remove unnecessary 'origin' headers to match other tests
set status code to 201 for MFA challenge creation endpoint
2025-12-10 01:58:47 +05:30
Steven NguyenandGitHub bdfb9e305e Merge pull request #10924 from appwrite/chore-bump-docker-base
chore: bump base docker image version to 0.10.6 to fix vulnerabilities
2025-12-09 12:15:39 -08:00
Steven NguyenandGitHub 6c1cbfddea Merge pull request #10922 from appwrite/copilot/fix-delayed-executions-trigger
Fix: Prevent job loss on container shutdown for delayed executions
2025-12-09 10:26:20 -08:00
VijaykumarPujar-tech e0a937912c Added Validation check for username and password 2025-12-09 23:46:44 +05:30
VijaykumarPujar-tech 8951a8465c Added the projects.php changes back 2025-12-09 23:30:34 +05:30
VijaykumarPujar-tech 2fa95b52a0 Reverted some added changes 2025-12-09 22:40:51 +05:30
Ujjwaljain16 676d53b18e fix: resolve MFA recovery code validation in 1.8.0
Remove strtolower() from recovery code type comparison (line 4945)
Remove strtolower() from match statement (line 4967)
Add comprehensive test for recovery code challenge validation
Fixes issue where recovery codes fail with 'Invalid token' error

Fixes #10740
2025-12-09 02:18:40 +05:30
Steven NguyenandGitHub 3a3c92140b Merge pull request #10916 from appwrite/copilot/fix-schedule-executions-bug
Fix ScheduleExecutions: inverted comparison prevented execution scheduling
2025-12-08 12:22:55 -08:00
Steven NguyenandGitHub cb3b22c9f9 chore: bump base docker image version to 0.10.6 to fix vulnerabilities
Our security scans reported several packages we used were out of date and required updates to patch security vulnerabilities.
2025-12-08 20:19:17 +00:00
copilot-swe-agent[bot]andstnguyen90 e4ebf755e4 Fix: Move schedule deletion inside coroutine to prevent job loss
Move the schedule document deletion and unset operation inside the coroutine, after the function is triggered. This ensures the schedule is only deleted after the job is successfully queued, preventing job loss if the container shuts down between coroutine start and completion.

This follows the same pattern used in ScheduleMessages.php.

Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-12-08 19:50:32 +00:00
copilot-swe-agent[bot] 6a712e236d Initial plan 2025-12-08 19:46:13 +00:00
Steven NguyenandGitHub 1cf8b103a8 chore: only sleep if delay is greater than 0 seconds
This prevents the warning:

Swoole\Coroutine::sleep(): Timer must be greater than or equal to 0.001
2025-12-08 19:23:35 +00:00
shimon 3ca9468b11 Merge branch '1.8.x' of github.com:appwrite/appwrite into refactor-functions-schedule 2025-12-08 17:50:06 +02:00
shimon 6156b97ea0 Refactor error handling in ScheduleBase class to improve project and resource validation. 2025-12-08 17:49:10 +02:00
Vijaykumar PujarandGitHub 0f84f21bb4 Merge branch 'main' into new_branch 2025-12-08 20:31:25 +05:30
VijaykumarPujar-tech a14d5c584e Fix: robust SMTP validation and added regression test 2025-12-08 20:22:18 +05:30
Jake BarnbyandGitHub 689e61c84f Merge pull request #10917 from appwrite/fix-array-index
Block array indexes
2025-12-08 10:51:06 +00:00
Jake Barnby aec7f27ce4 Fix tests 2025-12-08 23:20:40 +13:00
Luke B. SilverandGitHub c7bdf2a99d Merge pull request #10907 from appwrite/ser-650-2
Update utopia-php/dns version
2025-12-08 09:27:18 +00:00
shimon b79ff7fbff sync with 1.8.x 2025-12-08 09:31:49 +02:00
shimon 0a0cf26269 Merge branch '1.8.x' of github.com:appwrite/appwrite into refactor-auth-single-instance
# Conflicts:
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php
2025-12-08 09:26:38 +02:00
Jake Barnby 425bd514c2 Block array indexes 2025-12-08 18:54:46 +13:00
copilot-swe-agent[bot]andstnguyen90 be5f066dc3 Fix ScheduleExecutions bug: correct comparison operator to properly execute scheduled tasks
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-12-07 21:53:18 +00:00
copilot-swe-agent[bot] b2155acc56 Initial plan 2025-12-07 21:48:09 +00:00
Shimon NewmanGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
a241eab792 Update src/Appwrite/Platform/Tasks/ScheduleBase.php
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-12-07 18:26:14 +02:00
shimon 8ed4fa6ff1 composer 2025-12-07 17:03:18 +02:00
shimon 4289215620 Merge branch '1.8.x' of github.com:appwrite/appwrite into refactor-functions-schedule 2025-12-07 16:59:28 +02:00
shimon 7462b7ae17 Refactor ScheduleBase class to enhance schedule loading and project management. 2025-12-07 16:57:30 +02:00
Aditya Oberai decf2c7a1b add new site templates for CRM dashboard and job applications form 2025-12-05 18:51:22 +00:00
Aditya Oberai 43d57bbe4e Merge remote-tracking branch 'origin/1.8.x' into chore-add-new-site-templates 2025-12-05 16:19:08 +00:00
Hemachandar a26efc7aa5 Update utopia-php/dns version 2025-12-05 17:48:10 +05:30
Steven NguyenandGitHub bd288ce650 Merge pull request #10898 from appwrite/copilot/update-changes-md-for-1-8-1
Add CHANGES.md section for version 1.8.1
2025-12-04 17:36:59 -08:00
copilot-swe-agent[bot]andstnguyen90 9f2105b294 Address additional review feedback on CHANGES.md
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-12-05 00:48:50 +00:00
Steven NguyenandGitHub b3944c678c Merge pull request #10903 from appwrite/copilot/add-migration-version-for-1-8-1
Complete V23 migration for 1.8.1: add user email attributes and fix fall-through bug
2025-12-04 16:34:33 -08:00
copilot-swe-agent[bot]andstnguyen90 31c8c09060 Address review feedback on CHANGES.md categorization
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-12-05 00:28:49 +00:00
copilot-swe-agent[bot]andstnguyen90 4283671d49 Add user email attributes migration and fix missing break statement in V23
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-12-04 22:15:50 +00:00
copilot-swe-agent[bot] c3a3717bde Initial plan 2025-12-04 22:02:02 +00:00
Aditya OberaiandGitHub 27ede0079f Merge branch '1.7.x' into chore-add-new-site-templates 2025-12-05 02:58:06 +05:30
copilot-swe-agent[bot]andstnguyen90 5cdb59142f Add CHANGES.md section for version 1.8.1
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-12-03 22:11:54 +00:00
copilot-swe-agent[bot] bf6f784826 Initial plan 2025-12-03 22:03:16 +00:00
Steven NguyenandGitHub 33f7fd259b Merge pull request #10897 from appwrite/chore-bump-console-7.5.7
Bump console to version 7.5.7
2025-12-03 13:53:35 -08:00
Steven Nguyen bc99e04b57 feat: bump console to version 7.5.7 2025-12-03 13:26:06 -08:00
Steven Nguyen 10b8f97e96 chore: bump appwrite version to 1.8.1 2025-12-03 13:22:39 -08:00
shimon 4713ae5baa composer format 2025-12-03 21:39:59 +02:00
shimon e47802d1e1 Refactor Realtime message handling to improve authorization preservation and simplify error handling 2025-12-03 21:37:41 +02:00
shimon 77face0f2d Update utopia-php/storage to version 0.18.16 and clean up whitespace in realtime.php 2025-12-03 18:27:56 +02:00
shimon d483fbd43b Update composer.lock and refactor ScheduleBase class for improved schedule processing 2025-12-03 18:22:46 +02:00
shimon 13f886f07f Add authorization handling in Realtime connection management 2025-12-02 22:02:46 +02:00
Steven NguyenandGitHub c65e551697 Merge pull request #10797 from appwrite/update-readme-nov-2025
Update README files to match new tagline
2025-12-02 10:54:00 -08:00
Aditya OberaiandGitHub b4f9d97a81 Merge branch 'main' into update-readme-nov-2025 2025-12-02 23:30:39 +05:30
shimon 7041cae48c Refactor health check logic to simplify database failure handling 2025-12-02 12:58:55 +02:00
shimon b5537bb714 Merge branch 'refactor-auth-single-instance' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-12-02 12:57:15 +02:00
shimon 7392f6aeff Merge branch '1.8.x' of github.com:appwrite/appwrite into refactor-auth-single-instance
# Conflicts:
#	app/controllers/api/vcs.php
2025-12-02 12:49:15 +02:00
shimon c34649cb50 remove debug prints 2025-12-02 12:46:59 +02:00
shimon fde30b6216 Merge branch 'refactor-auth-single-instance' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-12-02 12:44:05 +02:00
shimon e0e9b2ba6f Remove debug output from RealtimeConsoleClientTest 2025-12-02 10:56:32 +02:00
shimon 250a7da1ee debug 2025-12-02 09:39:16 +02:00
shimon f208aa206d Merge branch 'refactor-auth-single-instance' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-12-02 09:29:29 +02:00
shimon d472978c1a debug 2025-12-02 09:29:24 +02:00
shimon 1d0c522fad Merge branch '1.8.x' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-12-01 16:52:45 +02:00
shimon beea3abfc7 remove debug prints 2025-12-01 15:50:15 +02:00
shimon 2dfa7d9542 Enhance health check logic to allow partial database failures without throwing an error, improving resilience in environments with multiple databases. 2025-12-01 13:40:34 +02:00
shimon 22c90ca7ad debug 2025-12-01 13:23:58 +02:00
shimon 2563a8f967 health api fix 2025-12-01 13:11:17 +02:00
shimon eb1c119c21 debug 2025-12-01 12:01:27 +02:00
shimon 27d88a421e debug 2025-12-01 11:59:21 +02:00
shimon e4755c624f debug 2025-12-01 11:45:01 +02:00
shimon 21ef33ea45 debug 2025-12-01 11:34:09 +02:00
shimon 5b2fdf7e5b tmp debug health db api 2025-12-01 11:19:25 +02:00
shimon cf6605746d Merge branch '1.8.x' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-11-30 14:26:52 +02:00
shimon b061f95f6d Refactor: Add missing Query import in Compute module and fix code formatting in V20 filter 2025-11-29 20:43:59 +02:00
shimon c5a45aaded Refactor: Remove unused parameters and clean up code in TablesDB and Sites modules 2025-11-29 19:13:18 +02:00
shimon d3fac8ae07 Refactor: Clean up code formatting in Compute, Functions, and Sites modules 2025-11-28 18:13:52 +02:00
shimon 013e961c37 Refactor: Remove unused authorization parameter from redeployVcsFunction calls and related methods 2025-11-28 18:13:03 +02:00
shimon b899e13763 Refactor: Update database authorization handling in realtime.php and regenerate composer.lock 2025-11-28 16:22:59 +02:00
shimon 5b3659af7c Remove unused Auth import in storage controller and fix formatting in UserTest class 2025-11-28 16:11:48 +02:00
Shimon NewmanandGitHub 95e29d1046 Merge branch '1.8.x' into refactor-auth-single-instance 2025-11-28 16:10:26 +02:00
shimon aed0079cad Fix: Correct role retrieval method call and update project test assertions for accuracy 2025-11-28 08:57:12 +02:00
shimon 67311fe5d6 Refactor: Update role retrieval to use authorization object in multiple files 2025-11-27 18:56:33 +02:00
shimon 5da1f791a5 sync with 1.8.x 2025-11-27 18:17:04 +02:00
shimon f216ea5d65 Merge branch '1.8.x' of github.com:appwrite/appwrite into refactor-auth-single-instance
# Conflicts:
#	app/controllers/api/account.php
#	app/controllers/api/graphql.php
#	app/controllers/api/storage.php
#	app/controllers/api/teams.php
#	app/controllers/general.php
#	app/controllers/shared/api.php
#	app/controllers/shared/api/auth.php
#	app/init/resources.php
#	app/realtime.php
#	app/worker.php
#	composer.lock
#	src/Appwrite/Auth/Auth.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
#	src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
#	src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php
#	src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php
#	src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php
#	src/Appwrite/Utopia/Request.php
#	src/Appwrite/Utopia/Response.php
#	tests/unit/Auth/AuthTest.php
#	tests/unit/Messaging/MessagingChannelsTest.php
2025-11-27 15:50:37 +02:00
shimon fb95a05599 teams update 2025-11-27 12:13:07 +02:00
shimon b76f01b144 Merge remote-tracking branch 'origin/refactor-auth-single-instance' into refactor-auth-single-instance 2025-11-27 11:54:36 +02:00
shimon 35e3294dc4 teams update 2025-11-27 11:51:53 +02:00
shimon 81c2ff501b Refactor: Adjust parameter order in getUserGitHub function calls for consistency 2025-11-27 11:26:53 +02:00
shimon 1bfd413040 Remove unnecessary authorization injection from StatsResources class 2025-11-26 22:32:30 +02:00
shimon c2ea1b5877 Add referenceType and reference parameters to Create deployment template 2025-11-26 19:52:17 +02:00
shimon 4dc4cc2c11 Update PHPUnit configuration to disable stopOnFailure 2025-11-26 17:39:31 +02:00
shimon 830eda7460 Refactor: Update functions to include Authorization parameter and enable stopOnFailure in PHPUnit configuration 2025-11-26 17:34:44 +02:00
shimon b69bf3d269 Revert previous revert of the merge for auth single instance refactor 2025-11-26 15:44:19 +02:00
shimon cee9906073 reverting the revert 2025-11-26 12:27:57 +02:00
shimon 4ad249f918 Revert "Revert "Merge pull request #10682 from appwrite/refactor-auth-single-instance""
This reverts commit 8765d5650a.
2025-11-26 12:05:12 +02:00
Matej BačoandGitHub b484dd8806 Merge pull request #10843 from appwrite/pla-3693
feat: docker-compose maildev update, traefik localhost domains
2025-11-20 11:37:08 +01:00
Levi van Noort 57758296f5 chore: include redis-insights auto-configuration 2025-11-20 11:00:58 +01:00
Levi van Noort a0a1cda2e5 chore: change to include adminir auto-login 2025-11-20 10:51:09 +01:00
Levi van Noort 26f506b7ba feat: provider locahost domain host for mail, redis and mysql 2025-11-19 17:25:28 +01:00
Levi van Noort 536537553b chore: change version mailcatcher to updated maildev 2.2.1 2025-11-19 17:04:01 +01:00
Ebenezer DonandGitHub 521a72f448 Merge branch 'main' into update-readme-nov-2025 2025-11-13 17:20:32 +00:00
Ebenezer Don d41746f29a update banner 2025-11-13 17:20:08 +00:00
Steven NguyenandGitHub b793a11cc8 Merge pull request #10775 from appwrite/fix/issue-triage-batch-mode
fix: Enable batch mode for issue triage safe-outputs
2025-11-12 17:27:56 -08:00
Ebenezer Don 9cda4d8928 update dashboard image 2025-11-12 23:14:14 +00:00
Ebenezer Don 6f351284e6 update banner 2025-11-12 23:11:08 +00:00
Steven Nguyen babddf2d1e fix: clarify bold formatting in triage instructions 2025-11-12 11:49:58 -08:00
Steven Nguyen 73d8fbfebc feat: update issue-triage to process comments for AI spam 2025-11-12 10:56:33 -08:00
Ebenezer Don 7d568af9cc Update README files to match new tagline and remove no-longer-needed stuff 2025-11-10 21:15:40 +00:00
Steven Nguyen 09933ce658 refactor: Simplify issue retrieval - use single since query
The list_issues tool's 'since' parameter already filters by updated_at,
which captures both newly created issues AND recently commented issues in
a single query. Removed redundant second query.
2025-11-07 12:28:14 -08:00
Steven Nguyen b225503dc5 feat: Enhance issue triage with updated criteria
- Include issues updated in last 24 hours (with new comments), not just newly created
- Add non-English content check: detect and request English translation respectfully
- Add multi-topic detection: ask users to split issues with multiple unrelated topics
- Improve spam detection with clear guidance
- Enhanced with empathetic approach for language and topic guidance
- Add parameter hints for list_label tool usage
- Clarify good first issue label application criteria
- Update summary message to reflect new triage scope
2025-11-07 12:26:13 -08:00
Steven Nguyen a77a279539 fix: update issue-triage workflow
- look for duplicate issues across the org
- fix deprecated timeout_minutes
2025-11-07 12:17:51 -08:00
Steven Nguyen b1e65725b5 fix: increase safe-outputs limits (labels=100, comments=10) and extend workflow stop-time 2025-11-07 12:01:42 -08:00
Steven Nguyen 84ebbc6993 fix: Enable labels toolset in GitHub MCP server
- Add github tool configuration with 'default' and 'labels' toolsets
- The labels toolset was not enabled by default, causing the agent
  to fall back to bash commands like 'gh label list'
- Now list_label MCP tool will be available to the agent
2025-11-06 15:43:31 -08:00
Steven Nguyen 94db75fd6c fix: Correct GitHub MCP tool name from list_labels to list_label
The correct tool name in GitHub MCP server is 'list_label' (singular), not 'list_labels' (plural). This was causing the agent to fall back to bash commands like 'gh label list' instead of using the proper MCP tool.
2025-11-06 15:05:45 -08:00
Steven Nguyen 2e4397a951 fix: Use GitHub MCP list_labels tool instead of bash gh command
- Replace 'gh label list' bash command instruction with 'list_labels' GitHub MCP tool
- GitHub MCP tools are already enabled and authenticated, no additional config needed
- Fixes permission denied errors when agent tried to use gh CLI
2025-11-06 14:33:24 -08:00
Steven Nguyen 2f255ce01c fix: Enable batch mode for issue triage safe-outputs
- Add target: "*" to safe-outputs configuration for add-labels and add-comment
- This allows the workflow to apply comments and labels to multiple issues
  when running in scheduled/manual trigger mode (batch processing)
- Previously, safe-outputs jobs were skipped because they required
  github.event.issue.number which only exists in event-triggered runs
- With target: "*", the workflow now reads issue numbers from the agent
  output and applies changes to each issue individually
2025-11-06 11:50:38 -08:00
Jake BarnbyandGitHub a94d33649b Merge pull request #10493 from appwrite/fix-check-1.7.x
Move disable
2025-09-22 19:51:40 +12:00
Jake Barnby 6d25eb81b6 Move disable 2025-09-15 13:09:08 +12:00
Tejas Raskar 80ad740d36 docs: update the directory structure in CONTRIBUTING.md 2025-08-20 14:34:43 +05:30
Khushboo Verma 8d7c76a434 Add screenshots for 2 templates 2025-06-20 12:53:06 +05:30
Khushboo Verma c927a1f459 Merge branch '1.7.x' into chore-add-new-site-templates 2025-06-20 12:09:34 +05:30
Khushboo Verma 88239a23f9 Update env vars for react-admin 2025-06-20 12:07:24 +05:30
Khushboo Verma 651af6a2c5 Add formspree, clerk and react-admin templates to sites 2025-06-19 21:02:31 +05:30
7386 changed files with 87898 additions and 23847 deletions
+31
View File
@@ -0,0 +1,31 @@
# PR Workflow for Appwrite
## Branch Targeting
**Important:** Appwrite does NOT use `main` as the primary development branch.
PRs should target the **current version branch** (e.g., `1.8.x`, `1.9.x`), not `main`.
### How to determine the correct target branch
1. Check which version branch you're currently on: `git branch --show-current`
2. Look for branches matching the pattern `X.Y.x` (e.g., `1.8.x`, `1.9.x`)
3. The current active development branch is typically the highest version number with the `.x` suffix
### When creating PRs
Always use the version branch as the base:
```bash
# Correct - targets the version branch
gh pr create --base 1.8.x --title "Your PR title" --body "..."
# Wrong - do not target main
gh pr create --base main ...
```
### Branch naming convention
- `X.Y.x` branches (e.g., `1.8.x`) - Active development branches for each minor version
- `main` - Not used for regular PRs; reserved for release management
- Feature branches should be created from and merged back into the current version branch
+14 -8
View File
@@ -2,6 +2,7 @@ _APP_ENV=development
_APP_EDITION=self-hosted
_APP_LOCALE=en
_APP_WORKER_PER_CORE=6
_APP_COMPRESSION_ENABLED=enabled
_APP_COMPRESSION_MIN_SIZE_BYTES=1024
_APP_CONSOLE_WHITELIST_ROOT=disabled
_APP_CONSOLE_WHITELIST_EMAILS=
@@ -21,13 +22,13 @@ _APP_OPTIONS_ROUTER_PROTECTION=disabled
_APP_OPTIONS_FORCE_HTTPS=disabled
_APP_OPTIONS_ROUTER_FORCE_HTTPS=disabled
_APP_OPENSSL_KEY_V1=your-secret-key
_APP_DNS=8.8.8.8
_APP_DOMAIN=traefik
_APP_DNS=172.16.238.100 # CoreDNS
_APP_DOMAIN=appwrite.test
_APP_CONSOLE_DOMAIN=localhost
_APP_DOMAIN_FUNCTIONS=functions.localhost
_APP_DOMAIN_SITES=sites.localhost
_APP_DOMAIN_TARGET_CNAME=test.localhost
_APP_DOMAIN_TARGET_A=127.0.0.1
_APP_DOMAIN_SITES=sites.localhost,rebranded.localhost
_APP_DOMAIN_TARGET_CNAME=cname.localhost
_APP_DOMAIN_TARGET_A=203.0.0.1
_APP_DOMAIN_TARGET_AAAA=::1
_APP_DOMAIN_TARGET_CAA=digicert.com
_APP_RULES_FORMAT=md5
@@ -69,8 +70,8 @@ _APP_STORAGE_ANTIVIRUS_PORT=3310
_APP_SMTP_HOST=maildev
_APP_SMTP_PORT=1025
_APP_SMTP_SECURE=
_APP_SMTP_USERNAME=
_APP_SMTP_PASSWORD=
_APP_SMTP_USERNAME=user
_APP_SMTP_PASSWORD=password
_APP_SMS_PROVIDER=sms://username:password@mock
_APP_SMS_FROM=+123456789
_APP_SMS_PROJECTS_DENY_LIST=
@@ -101,9 +102,12 @@ _APP_USAGE_AGGREGATION_INTERVAL=30
_APP_STATS_RESOURCES_INTERVAL=30
_APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000
_APP_MAINTENANCE_RETENTION_SCHEDULES=86400
_APP_INTERVAL_DOMAIN_VERIFICATION=120
_APP_INTERVAL_CLEANUP_STALE_EXECUTIONS=300
_APP_USAGE_STATS=enabled
_APP_LOGGING_CONFIG=
_APP_LOGGING_CONFIG_REALTIME=
_APP_GRAPHQL_INTROSPECTION=enabled
_APP_GRAPHQL_MAX_BATCH_SIZE=10
_APP_GRAPHQL_MAX_COMPLEXITY=250
_APP_GRAPHQL_MAX_DEPTH=4
@@ -124,4 +128,6 @@ _APP_MESSAGE_PUSH_TEST_DSN=
_APP_WEBHOOK_MAX_FAILED_ATTEMPTS=10
_APP_PROJECT_REGIONS=default
_APP_FUNCTIONS_CREATION_ABUSE_LIMIT=5000
_APP_STATS_USAGE_DUAL_WRITING_DBS=database_db_main
_APP_STATS_USAGE_DUAL_WRITING_DBS=database_db_main
_APP_TRUSTED_HEADERS=x-forwarded-for
_APP_POOL_ADAPTER=stack
+401 -215
View File
File diff suppressed because it is too large Load Diff
+53 -23
View File
@@ -8,41 +8,68 @@ on:
permissions: read-all
# Add stricter error-detection patterns so issue text doesn't trigger agent error detection.
# After merging this file run `gh aw compile` to regenerate the lock file.
env:
GH_AW_ERROR_PATTERNS: >-
[
{"id":"gh-action-error","pattern":"^::(error)(?:\\\\s+[^:]*)?::(.+)","level_group":1,"message_group":2,"description":"GitHub Actions workflow command - error"},
{"id":"gh-action-warning","pattern":"^::(warning)(?:\\\\s+[^:]*)?::(.+)","level_group":1,"message_group":2,"description":"GitHub Actions workflow command - warning"},
{"id":"bracketed-level","pattern":"^\\[(ERROR|CRITICAL|WARNING|WARN)\\]\\s+(.+)","level_group":1,"message_group":2,"description":"Bracketed log level at start of line"},
{"id":"timestamped-copilot","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z\\s+\\[(ERROR|WARN|WARNING|CRITICAL)\\]\\s+(.+)","level_group":1,"message_group":3,"description":"Timestamped Copilot CLI messages"}
]
network: defaults
safe-outputs:
add-labels:
max: 5
max: 100
target: "*"
add-comment:
max: 10
target: "*"
tools:
web-fetch:
web-search:
github:
toolsets:
- default
- labels
timeout_minutes: 10
timeout-minutes: 10
source: githubnext/agentics/workflows/issue-triage.md@0837fb7b24c3b84ee77fb7c8cfa8735c48be347a
---
# Agentic Triage
<!-- Note - this file can be customized to your needs. Replace this section directly, or add further instructions here. After editing run 'gh aw compile' -->
You're a triage assistant for GitHub issues. Your task is to analyze issues created in the last 24 hours and perform initial triage tasks for each of them.
You're a triage assistant for GitHub issues. Your task is to analyze issues that were either created in the last 24 hours or updated (with a new comment) in the last 24 hours, and perform initial triage tasks for each of them.
1. First, use the `list_issues` tool to retrieve all issues created in the last 24 hours. Filter issues by using the `since` parameter with a timestamp from 24 hours ago (calculate: current time minus 24 hours in ISO 8601 format).
1. First, use the `list_issues` tool to retrieve all issues created or updated in the last 24 hours. The `since` parameter filters by the issue's `updated_at` timestamp, which includes both newly created issues and recently commented issues. Calculate the timestamp from 24 hours ago (example: 2025-11-06T20:27:14Z for reference) and use it for the `since` parameter.
2. For each issue found, perform the following triage tasks:
3. Select appropriate labels for the issue from the provided list.
3. Use the `get_comments` tool to retrieve all the comments on the issue.
4. Retrieve the issue content using the `get_issue` tool. If the issue is obviously spam, or generated by bot, or something else that is not an actual issue to be worked on, then add an issue comment to the issue with a one sentence analysis and move to the next issue.
4. Check for spam and quality issue descriptions and comments first:
- **Non-English Content**: If the issue is primarily written in a non-English language, add a respectful and appreciative comment explaining that while you appreciate their contribution, the majority of the community communicates in English and kindly ask them to repost in English so everyone can follow along and help. Provide a friendly translation of your message in their language if possible.
- **Multiple Topics**: If the issue discusses multiple unrelated topics or problems, add a comment explaining that each issue should focus on one clear topic so the team can effectively solve the right problem. Politely ask them to split it into separate issues.
- **Obvious Spam or Bot-Generated Content**: If the issue/comment is obviously spam, generated by a bot, or something that is not an actual issue to be worked on, add an issue comment with a one-sentence analysis and move to the next issue.
5. Next, use the GitHub tools to gather additional context about the issue:
5. Retrieve the issue content using the `get_issue` tool for any issues that pass the spam checks.
- Fetch the list of labels available in this repository. Use 'gh label list' bash command to fetch the labels. This will give you the labels you can use for triaging issues.
- Fetch any comments on the issue using the `get_issue_comments` tool
- **Search for duplicate and related issues**: Use the `search_issues` tool to find similar issues by searching for key terms from the issue title and description. Look for both open and closed issues that might be related or duplicates.
6. Next, use the GitHub tools to gather additional context about the issue:
6. Analyze the issue content, considering:
- Fetch the list of labels available in this repository using the `list_label` tool with `owner: "appwrite"` and `repo: "appwrite"` parameters. This will give you the labels you can use for triaging issues.
- Fetch any comments on the issue using the `get_issue_comments` tool to understand recent activity
- **Search for duplicate and related issues (repo first, then org-wide)**:
- First search in this repository using the `search_issues` tool with a query like: `repo:appwrite/appwrite is:issue (is:open OR is:closed) <key terms>`.
- Then perform an org-wide search across the entire Appwrite organization using: `org:appwrite is:issue (is:open OR is:closed) <key terms>`.
- Prefer linking to OPEN issues when identifying potential duplicates; include CLOSED ones as related history when useful.
7. Analyze the issue content, considering:
- The issue title and description
- The type of issue (bug report, feature request, question, etc.)
@@ -51,35 +78,38 @@ You're a triage assistant for GitHub issues. Your task is to analyze issues crea
- User impact
- Components affected
7. Write notes, ideas, nudges, resource links, debugging strategies and/or reproduction steps for the team to consider relevant to the issue.
8. Write notes, ideas, nudges, resource links, debugging strategies and/or reproduction steps for the team to consider relevant to the issue.
8. Select appropriate labels from the available labels list provided above:
9. Select appropriate labels from the available labels list:
- Choose labels that accurately reflect the issue's nature
- Be specific but comprehensive
- Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority)
- Consider platform labels (android, ios) if applicable
- Search for similar issues, and if you find similar issues consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
- Only select labels from the provided list above
- Search for similar issues. If you find a duplicate of another OPEN issue in THIS repository, you may use a "duplicate" label (if available) and reference the canonical issue.
- If the closest match is in another repository within the Appwrite org, do NOT mark as duplicate here; instead, link it in your comment under a "Crossrepo related issues" section.
- Only select labels from the provided list
- Don't apply the `good first issue` or `help wanted` labels
- It's okay to not add any labels if none are clearly applicable
9. Apply the selected labels:
10. Apply the selected labels:
- Use the `update_issue` tool to apply the labels to the issue
- DO NOT communicate directly with users
- If no labels are clearly applicable, do not apply any labels
10. Add an issue comment to the issue with your analysis:
11. Add an issue comment to the issue with your analysis:
- Start with "🎯 Agentic Issue Triage"
- Provide a brief summary of the issue
- **If duplicate or related issues were found**, add a section listing them with links (e.g., "### 🔗 Potentially Related Issues" followed by a bullet list of related issues with their titles and links)
- **If duplicate or related issues were found**, add sections listing them with links:
- "### 🔗 Potentially Related Issues (this repo)" bullet list of same-repo issues with titles and links
- If applicable: "### 🌐 Cross-repo related issues (org: appwrite)" bullet list including `owner/repo#number` with titles and links
- Mention any relevant details that might help the team understand the issue better
- Include any debugging strategies or reproduction steps if applicable
- Suggest resources or links that might be helpful for resolving the issue or learning skills related to the issue or the particular area of the codebase affected by it
- Mention any nudges or ideas that could help the team in addressing the issue
- If you have possible reproduction steps, include them in the comment
- If you have any debugging strategies, include them in the comment
- If appropriate break the issue down to sub-tasks and write a checklist of things to do.
- Use collapsed-by-default sections in the GitHub markdown to keep the comment tidy. Collapse all sections except the short main summary at the top.
- If appropriate break the issue down to sub-tasks and write a checklist of things to do
- Use collapsed-by-default sections in the GitHub markdown to keep the comment tidy. Collapse all sections except the short main summary at the top. For bolded section titles, wrap the text with `<strong>` and `</strong>` to make it bold.
- Do not indicate/encourage a community member to submit a PR for the issue.
11. After processing all issues, provide a summary of how many issues were triaged. If no issues were created in the last 24 hours, simply note that no new issues needed triage.
12. After processing all issues, provide a summary of how many issues were triaged (created or updated in the last 24 hours). If no issues matched the criteria, simply note that no issues needed triage.
+1
View File
@@ -42,6 +42,7 @@ jobs:
with:
context: .
platforms: linux/amd64,linux/arm64
target: production
build-args: |
VERSION=${{ steps.meta.outputs.version }}
VITE_APPWRITE_GROWTH_ENDPOINT=https://growth.appwrite.io/v1
+3 -2
View File
@@ -20,10 +20,10 @@ jobs:
submodules: recursive
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
@@ -46,6 +46,7 @@ jobs:
with:
context: .
platforms: linux/amd64,linux/arm64
target: production
build-args: |
VERSION=${{ steps.meta.outputs.version }}
push: true
+11 -11
View File
@@ -223,7 +223,7 @@ jobs:
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group devKeys,screenshots
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group abuseEnabled,screenshots
- name: Failure Logs
if: failure()
@@ -312,7 +312,7 @@ jobs:
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group devKeys,screenshots
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group abuseEnabled,screenshots
- name: Failure Logs
if: failure()
@@ -322,8 +322,8 @@ jobs:
echo "=== OpenRuntimes Executor Logs ==="
docker compose logs openruntimes-executor
e2e_dev_keys:
name: E2E Service Test (Dev Keys)
e2e_abuse_enabled:
name: E2E Service Test (Abuse enabled)
runs-on: ubuntu-latest
needs: setup
steps:
@@ -344,7 +344,7 @@ jobs:
docker compose up -d
sleep 30
- name: Run Projects tests with dev keys in dedicated table mode
- name: Run Projects tests in dedicated table mode
run: |
echo "Using project tables"
export _APP_DATABASE_SHARED_TABLES=
@@ -354,7 +354,7 @@ jobs:
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=devKeys
appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=abuseEnabled
- name: Failure Logs
if: failure()
@@ -364,8 +364,8 @@ jobs:
echo "=== OpenRuntimes Executor Logs ==="
docker compose logs openruntimes-executor
e2e_dev_keys_shared_mode:
name: E2E Shared Mode Service Test (Dev Keys)
e2e_abuse_enabled_shared_mode:
name: E2E Shared Mode Service Test (Abuse enabled)
runs-on: ubuntu-latest
needs: [ setup, check_database_changes ]
if: needs.check_database_changes.outputs.database_changed == 'true'
@@ -394,7 +394,7 @@ jobs:
docker compose up -d
sleep 30
- name: Run Projects tests with dev keys in ${{ matrix.tables-mode }} table mode
- name: Run Projects tests in ${{ matrix.tables-mode }} table mode
run: |
if [ "${{ matrix.tables-mode }}" == "Shared V1" ]; then
echo "Using shared tables V1"
@@ -410,7 +410,7 @@ jobs:
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=devKeys
appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=abuseEnabled
- name: Failure Logs
if: failure()
@@ -420,7 +420,7 @@ jobs:
echo "=== OpenRuntimes Executor Logs ==="
docker compose logs openruntimes-executor
e2e_screenshots_keys:
e2e_screenshots:
name: E2E Service Test (Site Screenshots)
runs-on: ubuntu-latest
needs: setup
+77
View File
@@ -0,0 +1,77 @@
# AGENTS.md
Appwrite is an end-to-end backend server for web, mobile, native, and backend apps. This guide provides context and instructions for AI coding agents working on the Appwrite codebase.
## Project Overview
Appwrite is a self-hosted Backend-as-a-Service (BaaS) platform that provides developers with a set of APIs and tools to build secure, scalable applications. The project uses a hybrid monolithic-microservice architecture built with PHP, running on Swoole for high performance.
**Key Technologies:**
- **Backend:** PHP 8.3+, Swoole
- **Libraries:** Utopia PHP
- **Database:** MariaDB, Redis
- **Cache:** Redis
- **Queue:** Redis
- **Containers:** Docker
## Development Commands
```bash
# Run Appwrite
docker compose up -d --force-recreate --build
# Run specific test
docker compose exec appwrite test /usr/src/code/tests/e2e/Services/[ServiceName] --filter=[FunctionName]
# Format code
composer format
```
## Code Style Guidelines
- Follow [PSR-12](https://www.php-fig.org/psr/psr-12/) coding standard
- Use PSR-4 autoloading
- Strict type declarations where applicable
- Comprehensive PHPDoc comments
### Naming Conventions
#### `resourceType` Naming Rule
When a collection has a combination of `resourceType`, `resourceId`, and/or `resourceInternalId`, the value of `resourceType` MUST always be **plural** - for example: `functions`, `sites`, `deployments`.
Examples:
```php
'resourceType' => 'functions'
'resourceType' => 'sites'
'resourceType' => 'deployments'
```
## Security Considerations
### Critical Security Practices
- **Never hardcode credentials** - Use environment variables
- **Rate limiting** - Respect abuse prevention mechanisms
## Dependencies
Avoid introducing new dependencies other than utopia-php.
## Adding new endpoints
When adding new endpoints, make sure to use modules and follow its patterns. Find instruction in [Modules AGENTS.md](src/Appwrite/Platform/AGENTS.md) file.
## Pull Request Guidelines
### Before Submitting
- Run `composer format`
- Update documentation if adding features
- Add/update tests for your changes
- Check that Docker build succeeds
`docs/specs/authentication.drawio.svg`
## Known Issues and Gotchas
- **Hot Reload:** Code changes require container restart in some cases
- **Logging:** There is no central place for logs, so when debugging, ensure to check all possibly relevant containers
+98
View File
@@ -1,3 +1,101 @@
# Version 1.8.1
## What's Changed
### Notable changes
* Add branch deployments support in [#10486](https://github.com/appwrite/appwrite/pull/10486)
* Add TanStack Start sites support in [#10681](https://github.com/appwrite/appwrite/pull/10681)
* Add Next.js standalone support in [#10747](https://github.com/appwrite/appwrite/pull/10747)
* Add Resend integration in [#10690](https://github.com/appwrite/appwrite/pull/10690)
* Add option to enable/disable image transformations per-bucket in [#10722](https://github.com/appwrite/appwrite/pull/10722)
* Add operators support in [#10735](https://github.com/appwrite/appwrite/pull/10735) and [#10800](https://github.com/appwrite/appwrite/pull/10800)
* Add function and sites stats in [#10786](https://github.com/appwrite/appwrite/pull/10786)
* Add disable count feature in [#10668](https://github.com/appwrite/appwrite/pull/10668)
* Add ElevenLabs site template in [#10782](https://github.com/appwrite/appwrite/pull/10782)
* Add suggested environment variables in [#10795](https://github.com/appwrite/appwrite/pull/10795)
* Update GeoDB database in [#10890](https://github.com/appwrite/appwrite/pull/10890)
* Update Flutter default build runtime in [#10807](https://github.com/appwrite/appwrite/pull/10807)
* Upgrade runtimes in [#10804](https://github.com/appwrite/appwrite/pull/10804)
### Fixes
* Fix duplicate document error while creating file in [#10891](https://github.com/appwrite/appwrite/pull/10891)
* Fix "Update external deployment (authorize)" throwing 500 error due to invalid query in [#10888](https://github.com/appwrite/appwrite/pull/10888)
* Fix error setting user password in [#10889](https://github.com/appwrite/appwrite/pull/10889)
* Fix error generating email MFA challenges in [#10884](https://github.com/appwrite/appwrite/pull/10884)
* Fix file token expiry in [#10877](https://github.com/appwrite/appwrite/pull/10877)
* Fix TanStack Nitro default in [#10860](https://github.com/appwrite/appwrite/pull/10860)
* Fix TanStack builds in [#10767](https://github.com/appwrite/appwrite/pull/10767)
* Fix nullable validation in [#10819](https://github.com/appwrite/appwrite/pull/10819) and [#10778](https://github.com/appwrite/appwrite/pull/10778)
* Fix WebP library in [#10738](https://github.com/appwrite/appwrite/pull/10738)
* Fix batch writes in [#10812](https://github.com/appwrite/appwrite/pull/10812)
* Fix error handler error in [#10719](https://github.com/appwrite/appwrite/pull/10719)
* Fix Next 16 compatibility in [#10713](https://github.com/appwrite/appwrite/pull/10713)
* Fix stats usage memory leak in [#10683](https://github.com/appwrite/appwrite/pull/10683)
* Fix author URL in template deployments in [#10535](https://github.com/appwrite/appwrite/pull/10535)
* Fix VCS lock deletion in [#10691](https://github.com/appwrite/appwrite/pull/10691)
### Miscellaneous
* Add CSV export functionality in [#10546](https://github.com/appwrite/appwrite/pull/10546), [#10750](https://github.com/appwrite/appwrite/pull/10750), [#10813](https://github.com/appwrite/appwrite/pull/10813), and [#10847](https://github.com/appwrite/appwrite/pull/10847)
* Add JWT disposition in [#10867](https://github.com/appwrite/appwrite/pull/10867)
* Add screenshots endpoint in [#10675](https://github.com/appwrite/appwrite/pull/10675)
* Add screenshot endpoint stats in [#10706](https://github.com/appwrite/appwrite/pull/10706)
* Add users attributes in [#10688](https://github.com/appwrite/appwrite/pull/10688)
* Add max build duration environment variable in [#10674](https://github.com/appwrite/appwrite/pull/10674)
* Add custom realtime logger in [#10871](https://github.com/appwrite/appwrite/pull/10871)
* Add logs in [#10869](https://github.com/appwrite/appwrite/pull/10869)
* Improve MFA docs endpoint order in [#10793](https://github.com/appwrite/appwrite/pull/10793)
* Auth refactor in [#10758](https://github.com/appwrite/appwrite/pull/10758), [#10837](https://github.com/appwrite/appwrite/pull/10837), [#10682](https://github.com/appwrite/appwrite/pull/10682), and [#10667](https://github.com/appwrite/appwrite/pull/10667)
* Bump assistant to 0.8.4 in [#10887](https://github.com/appwrite/appwrite/pull/10887)
* Bump database to 3.1.5 in [#10766](https://github.com/appwrite/appwrite/pull/10766)
* Bump Utopia DNS in [#10761](https://github.com/appwrite/appwrite/pull/10761)
* Update domains to 0.8.3 in [#10658](https://github.com/appwrite/appwrite/pull/10658)
* Update domains to 0.9.1 in [#10678](https://github.com/appwrite/appwrite/pull/10678)
* Update Apple Swift to 13.3.0 in [#10679](https://github.com/appwrite/appwrite/pull/10679)
* Update Apple Swift in [#10663](https://github.com/appwrite/appwrite/pull/10663)
* Update CLI to 10.2.2 in [#10672](https://github.com/appwrite/appwrite/pull/10672)
* Update to CLI 12.0.0 in [#10853](https://github.com/appwrite/appwrite/pull/10853)
* Update docs examples to use Permission class in [#10707](https://github.com/appwrite/appwrite/pull/10707)
* Update SDK examples docs in [#10855](https://github.com/appwrite/appwrite/pull/10855)
* Release Python SDK in [#10762](https://github.com/appwrite/appwrite/pull/10762)
* Release Flutter 20.3.2 in [#10838](https://github.com/appwrite/appwrite/pull/10838)
* Release Flutter/Dart add screenshot examples in [#10811](https://github.com/appwrite/appwrite/pull/10811)
* Release PHP CLI in [#10791](https://github.com/appwrite/appwrite/pull/10791)
* Release SDKs in [#10817](https://github.com/appwrite/appwrite/pull/10817)
* Update SDKs in [#10694](https://github.com/appwrite/appwrite/pull/10694), [#10729](https://github.com/appwrite/appwrite/pull/10729), and [#10744](https://github.com/appwrite/appwrite/pull/10744)
* Update SDK generator in [#10743](https://github.com/appwrite/appwrite/pull/10743)
* Update database in [#10664](https://github.com/appwrite/appwrite/pull/10664)
* Update README file in [#10763](https://github.com/appwrite/appwrite/pull/10763)
* SDK release documentation in [#10745](https://github.com/appwrite/appwrite/pull/10745)
* SDK release runtime config in [#10765](https://github.com/appwrite/appwrite/pull/10765)
* Sync specs in [#10789](https://github.com/appwrite/appwrite/pull/10789)
* Sync 1.8.0 in [#10677](https://github.com/appwrite/appwrite/pull/10677)
* Add workflow for issue triage in [#10718](https://github.com/appwrite/appwrite/pull/10718)
* Add issue auto-labeler in [#10700](https://github.com/appwrite/appwrite/pull/10700)
* Add AI moderator repo in [#10717](https://github.com/appwrite/appwrite/pull/10717)
* Browser bump in [#10850](https://github.com/appwrite/appwrite/pull/10850)
* Template type enum override in [#10848](https://github.com/appwrite/appwrite/pull/10848)
* VCS reference type in [#10852](https://github.com/appwrite/appwrite/pull/10852)
* Index scope description in [#10851](https://github.com/appwrite/appwrite/pull/10851)
* Config for environment in [#10833](https://github.com/appwrite/appwrite/pull/10833)
* Format instance in [#10830](https://github.com/appwrite/appwrite/pull/10830)
* Replace sleep in webhooks service in [#10656](https://github.com/appwrite/appwrite/pull/10656)
* Update email composer in [#10720](https://github.com/appwrite/appwrite/pull/10720)
* Update facts on GitHub sites and functions in [#10593](https://github.com/appwrite/appwrite/pull/10593) and [#10771](https://github.com/appwrite/appwrite/pull/10771)
* Fix wrong user type in [#10875](https://github.com/appwrite/appwrite/pull/10875)
* Fix limit and offset computation in [#10880](https://github.com/appwrite/appwrite/pull/10880)
* Fix enum examples in [#10828](https://github.com/appwrite/appwrite/pull/10828)
* Fix response models multi-methods in [#10815](https://github.com/appwrite/appwrite/pull/10815)
* Fix undefined variable in [#10654](https://github.com/appwrite/appwrite/pull/10654)
* Fix undefined sequence in [#10652](https://github.com/appwrite/appwrite/pull/10652)
* Fix description in [#10702](https://github.com/appwrite/appwrite/pull/10702)
* Fix warning in builds worker in [#10705](https://github.com/appwrite/appwrite/pull/10705)
* Fix sites create deployment docs in [#10566](https://github.com/appwrite/appwrite/pull/10566)
* Fix test dependencies projects in [#10655](https://github.com/appwrite/appwrite/pull/10655)
* Fix list sites test in [#10726](https://github.com/appwrite/appwrite/pull/10726)
# Version 1.8.0
## What's Changed
+25 -10
View File
@@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
--no-plugins --no-scripts --prefer-dist \
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
FROM appwrite/base:0.10.5 AS final
FROM appwrite/base:0.11.5 AS base
LABEL maintainer="team@appwrite.io"
@@ -36,9 +36,7 @@ COPY --from=composer /usr/local/src/vendor /usr/src/code/vendor
COPY ./app /usr/src/code/app
COPY ./public /usr/src/code/public
COPY ./bin /usr/local/bin
COPY ./docs /usr/src/code/docs
COPY ./src /usr/src/code/src
COPY ./dev /usr/src/code/dev
# Set Volumes
RUN mkdir -p /storage/uploads && \
@@ -59,6 +57,7 @@ RUN mkdir -p /storage/uploads && \
# Executables
RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/install && \
chmod +x /usr/local/bin/interval && \
chmod +x /usr/local/bin/maintenance && \
chmod +x /usr/local/bin/migrate && \
chmod +x /usr/local/bin/realtime && \
@@ -78,6 +77,7 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/queue-count-success && \
chmod +x /usr/local/bin/worker-audits && \
chmod +x /usr/local/bin/worker-builds && \
chmod +x /usr/local/bin/worker-screenshots && \
chmod +x /usr/local/bin/worker-certificates && \
chmod +x /usr/local/bin/worker-databases && \
chmod +x /usr/local/bin/worker-deletes && \
@@ -90,15 +90,30 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/stats-resources && \
chmod +x /usr/local/bin/worker-stats-resources
# Letsencrypt Permissions
RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/
# Enable Extensions
RUN if [ "$DEBUG" = "true" ]; then cp /usr/src/code/dev/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini; fi
RUN if [ "$DEBUG" = "true" ]; then mkdir -p /tmp/xdebug; fi
RUN if [ "$DEBUG" = "true" ]; then apk add --update --no-cache openssh-client github-cli; fi
RUN if [ "$DEBUG" = "false" ]; then rm -rf /usr/src/code/dev; fi
RUN if [ "$DEBUG" = "false" ]; then rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20230831/xdebug.so; fi
FROM base AS production
RUN rm -rf /usr/src/code/app/config/specs && \
rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20240924/xdebug.so && \
find /usr -name '*.a' -delete 2>/dev/null || true && \
find /usr -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true && \
find /usr -name '*.pyc' -delete 2>/dev/null || true
EXPOSE 80
CMD [ "php", "app/http.php" ]
FROM base AS development
COPY ./docs /usr/src/code/docs
COPY ./dev /usr/src/code/dev
RUN if [ "$DEBUG" = "true" ]; then \
cp /usr/src/code/dev/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini && \
mkdir -p /tmp/xdebug && \
apk add --update --no-cache openssh-client github-cli; \
fi
EXPOSE 80
+26 -29
View File
@@ -2,7 +2,7 @@
<br />
<p align="center">
<a href="https://appwrite.io" target="_blank"><img src="./public/images/banner.png" alt="Appwrite banner with logo and slogan build like a team of hundreds""></a>
<a href="https://appwrite.io" target="_blank"><img src="./public/images/banner.png" alt="Appwrite banner, with logo and text saying "The Developer's Cloud""></a>
<br />
<br />
<b>适用于[Flutter/Vue/Angular/React/iOS/Android/* 等等平台 *]的完整后端服务</b>
@@ -36,7 +36,6 @@ Appwrite 可以提供给开发者用户验证,外部授权,用户数据读
内容:
- [开始](#开始)
- [安装](#安装)
- [Unix](#unix)
@@ -57,7 +56,8 @@ Appwrite 可以提供给开发者用户验证,外部授权,用户数据读
- [版权说明](#版权说明)
## 开始
要轻松开始使用Appwrite,您可以[**免费注册Appwrite Cloud**](https://cloud.appwrite.io/)。在Appwrite Cloud公开测试版期间,您可以完全免费使用Appwrite,而且我们不会收集您的信用卡信息。
要轻松开始使用 Appwrite,您可以[**免费注册 Appwrite Cloud**](https://cloud.appwrite.io/)。在 Appwrite Cloud 公开测试版期间,您可以完全免费使用 Appwrite,而且我们不会收集您的信用卡信息。
## 安装
@@ -72,7 +72,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.8.0
appwrite/appwrite:1.8.1
```
### Windows
@@ -84,7 +84,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.8.0
appwrite/appwrite:1.8.1
```
#### PowerShell
@@ -94,7 +94,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.8.0
appwrite/appwrite:1.8.1
```
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
@@ -119,22 +119,16 @@ docker run -it --rm `
<br /><sub><b>DigitalOcean</b></sub></a>
</a>
</td>
<td align="center" width="100" height="100">
<a href="https://gitpod.io/#https://github.com/appwrite/integration-for-gitpod">
<img width="50" height="39" src="public/images/integrations/gitpod-logo.svg" alt="Gitpod Logo" />
<br /><sub><b>Gitpod</b></sub></a>
</a>
</td>
<td align="center" width="100" height="100">
<a href="https://www.linode.com/marketplace/apps/appwrite/appwrite/">
<img width="50" height="39" src="public/images/integrations/akamai-logo.svg" alt="Akamai Logo" />
<br /><sub><b>Akamai Compute</b></sub></a>
<br /><sub><b>Akamai Compute</b></sub></a>
</a>
</td>
<td align="center" width="100" height="100">
<a href="https://aws.amazon.com/marketplace/pp/prodview-2hiaeo2px4md6">
<img width="50" height="39" src="public/images/integrations/aws-logo.svg" alt="AWS Logo" />
<br /><sub><b>AWS Marketplace</b></sub></a>
<br /><sub><b>AWS Marketplace</b></sub></a>
</a>
</td>
</tr>
@@ -188,25 +182,28 @@ docker run -it --rm `
以下是当前支持的平台和语言列表。如果您想帮助我们为您选择的平台添加支持,您可以访问我们的 [SDK 生成器](https://github.com/appwrite/sdk-generator) 项目并查看我们的 [贡献指南](https://github.com/appwrite/sdk-generator/blob/master/CONTRIBUTING.md)。
#### 客户端
* :white_check_mark: &nbsp; [Web](https://github.com/appwrite/sdk-for-web) (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [Flutter](https://github.com/appwrite/sdk-for-flutter) (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [Apple](https://github.com/appwrite/sdk-for-apple) - **公测** (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [Android](https://github.com/appwrite/sdk-for-android) (由 Appwrite 团队维护)
- :white_check_mark: &nbsp; [Web](https://github.com/appwrite/sdk-for-web) (由 Appwrite 团队维护)
- :white_check_mark: &nbsp; [Flutter](https://github.com/appwrite/sdk-for-flutter) (由 Appwrite 团队维护)
- :white_check_mark: &nbsp; [Apple](https://github.com/appwrite/sdk-for-apple) - **公测** (由 Appwrite 团队维护)
- :white_check_mark: &nbsp; [Android](https://github.com/appwrite/sdk-for-android) (由 Appwrite 团队维护)
#### 服务器
* :white_check_mark: &nbsp; [NodeJS](https://github.com/appwrite/sdk-for-node) (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [PHP](https://github.com/appwrite/sdk-for-php) (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [Dart](https://github.com/appwrite/sdk-for-dart) - (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [Deno](https://github.com/appwrite/sdk-for-deno) - **公测** (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [Ruby](https://github.com/appwrite/sdk-for-ruby) (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [Python](https://github.com/appwrite/sdk-for-python) (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [Kotlin](https://github.com/appwrite/sdk-for-kotlin) - **公测** (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [Apple](https://github.com/appwrite/sdk-for-apple) - **公测** (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [.NET](https://github.com/appwrite/sdk-for-dotnet) - **公测** (由 Appwrite 团队维护)
- :white_check_mark: &nbsp; [NodeJS](https://github.com/appwrite/sdk-for-node) (由 Appwrite 团队维护)
- :white_check_mark: &nbsp; [PHP](https://github.com/appwrite/sdk-for-php) (由 Appwrite 团队维护)
- :white_check_mark: &nbsp; [Dart](https://github.com/appwrite/sdk-for-dart) - (由 Appwrite 团队维护)
- :white_check_mark: &nbsp; [Deno](https://github.com/appwrite/sdk-for-deno) - **公测** (由 Appwrite 团队维护)
- :white_check_mark: &nbsp; [Ruby](https://github.com/appwrite/sdk-for-ruby) (由 Appwrite 团队维护)
- :white_check_mark: &nbsp; [Python](https://github.com/appwrite/sdk-for-python) (由 Appwrite 团队维护)
- :white_check_mark: &nbsp; [Kotlin](https://github.com/appwrite/sdk-for-kotlin) - **公测** (由 Appwrite 团队维护)
- :white_check_mark: &nbsp; [Apple](https://github.com/appwrite/sdk-for-apple) - **公测** (由 Appwrite 团队维护)
- :white_check_mark: &nbsp; [.NET](https://github.com/appwrite/sdk-for-dotnet) - **公测** (由 Appwrite 团队维护)
#### 开发者社区
* :white_check_mark: &nbsp; [Appcelerator Titanium](https://github.com/m1ga/ti.appwrite) (维护者 [Michael Gangolf](https://github.com/m1ga/))
* :white_check_mark: &nbsp; [Godot Engine](https://github.com/GodotNuts/appwrite-sdk) (维护者 [fenix-hub @GodotNuts](https://github.com/fenix-hub))
- :white_check_mark: &nbsp; [Appcelerator Titanium](https://github.com/m1ga/ti.appwrite) (维护者 [Michael Gangolf](https://github.com/m1ga/))
- :white_check_mark: &nbsp; [Godot Engine](https://github.com/GodotNuts/appwrite-sdk) (维护者 [fenix-hub @GodotNuts](https://github.com/fenix-hub))
找不到需要的的 SDK? - 欢迎通过发起 PR 来帮助我们完善 Appwrite 的软件生态环境 [SDK 生成器](https://github.com/appwrite/sdk-generator)!
+5 -18
View File
@@ -6,10 +6,10 @@
<br />
<p align="center">
<a href="https://appwrite.io" target="_blank"><img src="./public/images/banner.png" alt="Appwrite banner, with logo and text saying "Build Like a Team of Hundreds"></a>
<a href="https://appwrite.io" target="_blank"><img src="./public/images/banner.png" alt="Appwrite banner, with logo and text saying "The Developer's Cloud"></a>
<br />
<br />
<b>Appwrite is an all-in-one development platform for Web, Mobile, and Flutter applications. Use built-in backend infrastructure and web hosting, all from a single place. Built with the open source community and optimized for developer experience in the coding languages you love.</b>
<b>Appwrite is a best-in-class, developer-first platform that gives builders everything they need to create scalable, stable, and production-ready software, fast.</b>
<br />
<br />
</p>
@@ -32,13 +32,6 @@ Appwrite is an end-to-end platform for building Web, Mobile, Native, or Backend
Using Appwrite, you can easily integrate your app with user authentication and multiple sign-in methods, a database for storing and querying users and team data, storage and file management, image manipulation, Cloud Functions, messaging, and [more services](https://appwrite.io/docs).
<p align="center">
<br />
<a href="https://www.producthunt.com/posts/appwrite-2?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-appwrite-2" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=360315&theme=light&period=daily" alt="Appwrite - 100&#0037;&#0032;open&#0032;source&#0032;alternative&#0032;for&#0032;Firebase | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<br />
<br />
</p>
![Appwrite project dashboard showing various Appwrite features](public/images/github.png)
Find out more at: [https://appwrite.io](https://appwrite.io).
@@ -82,7 +75,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.8.0
appwrite/appwrite:1.8.1
```
### Windows
@@ -94,7 +87,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.8.0
appwrite/appwrite:1.8.1
```
#### PowerShell
@@ -104,7 +97,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.8.0
appwrite/appwrite:1.8.1
```
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.
@@ -129,12 +122,6 @@ Choose from one of the providers below:
<br /><sub><b>DigitalOcean</b></sub></a>
</a>
</td>
<td align="center" width="100" height="100">
<a href="https://gitpod.io/#https://github.com/appwrite/integration-for-gitpod">
<img width="50" height="39" src="public/images/integrations/gitpod-logo.svg" alt="Gitpod Logo" />
<br /><sub><b>Gitpod</b></sub></a>
</a>
</td>
<td align="center" width="100" height="100">
<a href="https://www.linode.com/marketplace/apps/appwrite/appwrite/">
<img width="50" height="39" src="public/images/integrations/akamai-logo.svg" alt="Akamai Logo" />
+35 -10
View File
@@ -30,6 +30,9 @@ use Utopia\Pools\Group;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Publisher;
use Utopia\Registry\Registry;
use Utopia\Span\Exporter;
use Utopia\Span\Span;
use Utopia\Span\Storage;
use Utopia\System\System;
use Utopia\Telemetry\Adapter\None as NoTelemetry;
@@ -41,8 +44,7 @@ Config::setParam('runtimes', (new Runtimes('v5'))->getAll(supported: false));
// require controllers after overwriting runtimes
require_once __DIR__ . '/controllers/general.php';
Authorization::disable();
global $register;
CLI::setResource('register', fn () => $register);
CLI::setResource('cache', function ($pools) {
@@ -60,7 +62,13 @@ CLI::setResource('pools', function (Registry $register) {
return $register->get('pools');
}, ['register']);
CLI::setResource('dbForPlatform', function ($pools, $cache) {
CLI::setResource('authorization', function () {
$authorization = new Authorization();
$authorization->disable();
return $authorization;
}, []);
CLI::setResource('dbForPlatform', function ($pools, $cache, $authorization) {
$sleep = 3;
$maxAttempts = 5;
$attempts = 0;
@@ -74,6 +82,8 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) {
$dbForPlatform = new Database($adapter, $cache);
$dbForPlatform
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', 'console');
@@ -99,7 +109,7 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) {
}
return $dbForPlatform;
}, ['pools', 'cache']);
}, ['pools', 'cache', 'authorization']);
CLI::setResource('console', function () {
return new Document(Config::getParam('console'));
@@ -110,10 +120,10 @@ CLI::setResource(
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
);
CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases) {
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -146,6 +156,7 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$databases[$dsn->getHost()] = $database;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
@@ -162,17 +173,19 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId());
return $database;
};
}, ['pools', 'dbForPlatform', 'cache']);
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) {
CLI::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, $database) {
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant((int)$project->getSequence());
return $database;
@@ -182,6 +195,8 @@ CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) {
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_TASK)
@@ -194,7 +209,7 @@ CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) {
return $database;
};
}, ['pools', 'cache']);
}, ['pools', 'cache', 'authorization']);
CLI::setResource('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']);
@@ -257,6 +272,14 @@ CLI::setResource('logError', function (Registry $register) {
$log->addExtra('trace', $error->getTraceAsString());
$log->addExtra('detailedTrace', $error->getTrace());
if ($error->getPrevious() !== null) {
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
$log->addExtra('previousMessage', $error->getPrevious()->getMessage());
}
$log->addExtra('previousFile', $error->getPrevious()->getFile());
$log->addExtra('previousLine', $error->getPrevious()->getLine());
}
$log->setAction($action);
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
@@ -306,4 +329,6 @@ $cli
$cli->shutdown()->action(fn () => Timer::clearAll());
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
Span::setStorage(new Storage\Coroutine());
Span::addExporter(new Exporter\Stdout());
run($cli->run(...));
+2 -2
View File
@@ -26,8 +26,8 @@ unset($common['files']);
$collections = [
'buckets' => $buckets,
'databases' => $databases,
'projects' => array_merge($projects, $common),
'console' => array_merge($platform, $common),
'projects' => array_merge_recursive($projects, $common),
'console' => array_merge_recursive($platform, $common),
'logs' => $logs,
];
+11
View File
@@ -1288,6 +1288,17 @@ return [
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('labels'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => false,
'default' => null,
'array' => true,
'filters' => [],
],
],
'indexes' => [
[
+93 -32
View File
@@ -6,7 +6,7 @@ use Utopia\Database\Helpers\ID;
$providers = Config::getParam('oAuthProviders', []);
return [
$platformCollections = [
'projects' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('projects'),
@@ -330,7 +330,18 @@ return [
'default' => null,
'array' => false,
'filters' => ['datetime'],
]
],
[
'$id' => ID::custom('labels'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => false,
'default' => [],
'array' => true,
'filters' => [],
],
],
'indexes' => [
[
@@ -622,24 +633,35 @@ return [
'name' => 'keys',
'attributes' => [
[
'$id' => ID::custom('projectInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('projectId'),
'$id' => ID::custom('resourceType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => 0,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('resourceId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('resourceInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
@@ -712,14 +734,14 @@ return [
],
'indexes' => [
[
'$id' => ID::custom('_key_project'),
'$id' => ID::custom('_key_resource'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
'attributes' => ['resourceType', 'resourceInternalId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => '_key_accessedAt',
'$id' => ID::custom('_key_accessedAt'),
'type' => Database::INDEX_KEY,
'attributes' => ['accessedAt'],
'lengths' => [],
@@ -1317,6 +1339,17 @@ return [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('logs'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 1000000,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
@@ -1351,21 +1384,21 @@ return [
'$id' => '_key_type',
'type' => Database::INDEX_KEY,
'attributes' => ['type'],
'lengths' => [32],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_trigger',
'type' => Database::INDEX_KEY,
'attributes' => ['trigger'],
'lengths' => [32],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_deploymentResourceType',
'type' => Database::INDEX_KEY,
'attributes' => ['deploymentResourceType'],
'lengths' => [32],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
@@ -1407,23 +1440,23 @@ return [
'$id' => ID::custom('_key_owner'),
'type' => Database::INDEX_KEY,
'attributes' => ['owner'],
'lengths' => [16],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_region'),
'type' => Database::INDEX_KEY,
'attributes' => ['region'],
'lengths' => [16],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_piid_riid_rt'),
'$id' => ID::custom('_key_piid_diid_drt'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'deploymentInternalId', 'deploymentResourceType'],
'lengths' => [],
'orders' => [],
],
[
'$id' => '_key_region_status_createdAt',
'type' => Database::INDEX_KEY,
'attributes' => ['region', 'status', '$createdAt'],
'lengths' => [],
'orders' => [],
],
],
],
@@ -1892,3 +1925,31 @@ return [
'indexes' => []
],
];
// Organization API keys subquery
$platformCollections['teams']['attributes'][] = [
'$id' => ID::custom('keys'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['subQueryOrganizationKeys'],
];
// Account API keys subquery
$platformCollections['users']['attributes'][] = [
'$id' => ID::custom('keys'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['subQueryAccountKeys'],
];
return $platformCollections;
+106
View File
@@ -567,6 +567,17 @@ return [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('deploymentRetention'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('deploymentInternalId'),
'type' => Database::VAR_STRING,
@@ -765,6 +776,17 @@ return [
'default' => null,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('startCommand'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 20000,
'signed' => true,
'required' => false,
'default' => null,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('specification'),
@@ -776,6 +798,28 @@ return [
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('buildSpecification'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => false,
'required' => false,
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('runtimeSpecification'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => false,
'required' => false,
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'$id' => ID::custom('scopes'),
'type' => Database::VAR_STRING,
@@ -1035,6 +1079,17 @@ return [
'default' => null,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('startCommand'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 20000,
'signed' => true,
'required' => false,
'default' => null,
'filters' => [],
],
[
'$id' => ID::custom('fallbackFile'),
'type' => Database::VAR_STRING,
@@ -1046,6 +1101,17 @@ return [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('deploymentRetention'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('deploymentInternalId'),
'type' => Database::VAR_STRING,
@@ -1200,6 +1266,28 @@ return [
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('buildSpecification'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => false,
'required' => false,
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('runtimeSpecification'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => false,
'required' => false,
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'$id' => ID::custom('buildRuntime'),
'type' => Database::VAR_STRING,
@@ -1357,6 +1445,17 @@ return [
'default' => null,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('startCommand'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 20000,
'signed' => true,
'required' => false,
'default' => null,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('buildOutput'),
@@ -2098,6 +2197,13 @@ return [
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_resourceType'),
'type' => Database::INDEX_KEY,
'attributes' => ['resourceType'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
],
],
+62 -38
View File
@@ -357,6 +357,11 @@ return [
'description' => 'API key and session used in the same request. Use either `setSession` or `setKey`. Learn about which authentication method to use in the SSR docs: https://appwrite.io/docs/products/auth/server-side-rendering',
'code' => 403,
],
Exception::USER_JWT_AND_COOKIE_SET => [
'name' => Exception::USER_JWT_AND_COOKIE_SET,
'description' => 'JWT and cookie used in the same request. Use either `setJWT` or `setCookie`. Learn about which authentication method to use in the SSR docs: https://appwrite.io/docs/products/auth/server-side-rendering',
'code' => 403,
],
Exception::API_KEY_EXPIRED => [
'name' => Exception::API_KEY_EXPIRED,
'description' => 'The dynamic API key has expired. Please don\'t use dynamic API keys for more than duration of the execution.',
@@ -688,12 +693,12 @@ return [
/** Databases */
Exception::DATABASE_NOT_FOUND => [
'name' => Exception::DATABASE_NOT_FOUND,
'description' => 'Database not found',
'description' => 'Database with the requested ID \'%s\' could not be found.',
'code' => 404
],
Exception::DATABASE_ALREADY_EXISTS => [
'name' => Exception::DATABASE_ALREADY_EXISTS,
'description' => 'Database already exists',
'description' => 'Database with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409
],
Exception::DATABASE_TIMEOUT => [
@@ -710,41 +715,41 @@ return [
/** Collections */
Exception::COLLECTION_NOT_FOUND => [
'name' => Exception::COLLECTION_NOT_FOUND,
'description' => 'Collection with the requested ID could not be found.',
'description' => 'Collection with the requested ID \'%s\' could not be found.',
'code' => 404,
],
Exception::COLLECTION_ALREADY_EXISTS => [
'name' => Exception::COLLECTION_ALREADY_EXISTS,
'description' => 'A collection with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'description' => 'A collection with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::COLLECTION_LIMIT_EXCEEDED => [
'name' => Exception::COLLECTION_LIMIT_EXCEEDED,
'description' => 'The maximum number of collections has been reached.',
'description' => 'The maximum number of collections for database \'%s\' has been reached.',
'code' => 400,
],
/** Tables */
Exception::TABLE_NOT_FOUND => [
'name' => Exception::TABLE_NOT_FOUND,
'description' => 'Table with the requested ID could not be found.',
'description' => 'Table with the requested ID \'%s\' could not be found.',
'code' => 404,
],
Exception::TABLE_ALREADY_EXISTS => [
'name' => Exception::TABLE_ALREADY_EXISTS,
'description' => 'A table with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'description' => 'A table with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::TABLE_LIMIT_EXCEEDED => [
'name' => Exception::TABLE_LIMIT_EXCEEDED,
'description' => 'The maximum number of tables has been reached.',
'description' => 'The maximum number of tables for database \'%s\' has been reached.',
'code' => 400,
],
/** Documents */
Exception::DOCUMENT_NOT_FOUND => [
'name' => Exception::DOCUMENT_NOT_FOUND,
'description' => 'Document with the requested ID could not be found.',
'description' => 'Document with the requested ID \'%s\' could not be found.',
'code' => 404,
],
Exception::DOCUMENT_INVALID_STRUCTURE => [
@@ -764,7 +769,7 @@ return [
],
Exception::DOCUMENT_ALREADY_EXISTS => [
'name' => Exception::DOCUMENT_ALREADY_EXISTS,
'description' => 'Document with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'description' => 'Document with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::DOCUMENT_UPDATE_CONFLICT => [
@@ -781,7 +786,7 @@ return [
/** Rows */
Exception::ROW_NOT_FOUND => [
'name' => Exception::ROW_NOT_FOUND,
'description' => 'Row with the requested ID could not be found.',
'description' => 'Row with the requested ID \'%s\' could not be found.',
'code' => 404,
],
Exception::ROW_INVALID_STRUCTURE => [
@@ -791,7 +796,7 @@ return [
],
Exception::ROW_MISSING_DATA => [
'name' => Exception::ROW_MISSING_DATA,
'description' => 'The row data is missing. Try again with row data populated',
'description' => 'The row data is missing. Try again with row data populated.',
'code' => 400,
],
Exception::ROW_MISSING_PAYLOAD => [
@@ -801,7 +806,7 @@ return [
],
Exception::ROW_ALREADY_EXISTS => [
'name' => Exception::ROW_ALREADY_EXISTS,
'description' => 'Row with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'description' => 'Row with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::ROW_UPDATE_CONFLICT => [
@@ -818,17 +823,17 @@ return [
/** Attributes */
Exception::ATTRIBUTE_NOT_FOUND => [
'name' => Exception::ATTRIBUTE_NOT_FOUND,
'description' => 'Attribute with the requested ID could not be found.',
'description' => 'Attribute with the requested key \'%s\' could not be found.',
'code' => 404,
],
Exception::ATTRIBUTE_UNKNOWN => [
'name' => Exception::ATTRIBUTE_UNKNOWN,
'description' => 'The attribute required for the index could not be found. Please confirm all your attributes are in the available state.',
'description' => 'The attribute \'%s\' required for the index could not be found. Please confirm all your attributes are in the available state.',
'code' => 400,
],
Exception::ATTRIBUTE_NOT_AVAILABLE => [
'name' => Exception::ATTRIBUTE_NOT_AVAILABLE,
'description' => 'The requested attribute is not yet available. Please try again later.',
'description' => 'The requested attribute \'%s\' is not yet available. Please try again later.',
'code' => 400,
],
Exception::ATTRIBUTE_FORMAT_UNSUPPORTED => [
@@ -843,12 +848,12 @@ return [
],
Exception::ATTRIBUTE_ALREADY_EXISTS => [
'name' => Exception::ATTRIBUTE_ALREADY_EXISTS,
'description' => 'Attribute with the requested key already exists. Attribute keys must be unique, try again with a different key.',
'description' => 'Attribute with the requested key \'%s\' already exists. Attribute keys must be unique, try again with a different key.',
'code' => 409,
],
Exception::ATTRIBUTE_LIMIT_EXCEEDED => [
'name' => Exception::ATTRIBUTE_LIMIT_EXCEEDED,
'description' => 'The maximum number or size of attributes for this collection has been reached.',
'description' => 'The maximum number or size of attributes for collection \'%s\' has been reached.',
'code' => 400,
],
Exception::ATTRIBUTE_VALUE_INVALID => [
@@ -858,7 +863,7 @@ return [
],
Exception::ATTRIBUTE_TYPE_INVALID => [
'name' => Exception::ATTRIBUTE_TYPE_INVALID,
'description' => 'The attribute type is invalid.',
'description' => 'The attribute \'%s\' type is invalid.',
'code' => 400,
],
Exception::ATTRIBUTE_INVALID_RESIZE => [
@@ -869,7 +874,7 @@ return [
Exception::ATTRIBUTE_TYPE_NOT_SUPPORTED => [
'name' => Exception::ATTRIBUTE_TYPE_NOT_SUPPORTED,
'description' => 'Attribute type is not supported.',
'description' => 'Attribute type \'%s\' is not supported.',
'code' => 400,
],
@@ -883,17 +888,17 @@ return [
/** Columns */
Exception::COLUMN_NOT_FOUND => [
'name' => Exception::COLUMN_NOT_FOUND,
'description' => 'Column with the requested ID could not be found.',
'description' => 'Column with the requested key \'%s\' could not be found.',
'code' => 404,
],
Exception::COLUMN_UNKNOWN => [
'name' => Exception::COLUMN_UNKNOWN,
'description' => 'The column required for the index could not be found. Please confirm all your columns are in the available state.',
'description' => 'The column \'%s\' required for the index could not be found. Please confirm all your columns are in the available state.',
'code' => 400,
],
Exception::COLUMN_NOT_AVAILABLE => [
'name' => Exception::COLUMN_NOT_AVAILABLE,
'description' => 'The requested column is not yet available. Please try again later.',
'description' => 'The requested column \'%s\' is not yet available. Please try again later.',
'code' => 400,
],
Exception::COLUMN_FORMAT_UNSUPPORTED => [
@@ -908,12 +913,12 @@ return [
],
Exception::COLUMN_ALREADY_EXISTS => [
'name' => Exception::COLUMN_ALREADY_EXISTS,
'description' => 'Column with the requested key already exists. Column keys must be unique, try again with a different key.',
'description' => 'Column with the requested key \'%s\' already exists. Column keys must be unique, try again with a different key.',
'code' => 409,
],
Exception::COLUMN_LIMIT_EXCEEDED => [
'name' => Exception::COLUMN_LIMIT_EXCEEDED,
'description' => 'The maximum number or size of columns for this table has been reached.',
'description' => 'The maximum number or size of columns for table \'%s\' has been reached.',
'code' => 400,
],
Exception::COLUMN_VALUE_INVALID => [
@@ -923,7 +928,7 @@ return [
],
Exception::COLUMN_TYPE_INVALID => [
'name' => Exception::COLUMN_TYPE_INVALID,
'description' => 'The column type is invalid.',
'description' => 'The column \'%s\' type is invalid.',
'code' => 400,
],
Exception::COLUMN_INVALID_RESIZE => [
@@ -933,24 +938,24 @@ return [
],
Exception::COLUMN_TYPE_NOT_SUPPORTED => [
'name' => Exception::COLUMN_TYPE_NOT_SUPPORTED,
'description' => 'Column type is not supported.',
'description' => 'Column type \'%s\' is not supported.',
'code' => 400,
],
/** Indexes */
Exception::INDEX_NOT_FOUND => [
'name' => Exception::INDEX_NOT_FOUND,
'description' => 'Index with the requested ID could not be found.',
'description' => 'Index with the requested key \'%s\' could not be found.',
'code' => 404,
],
Exception::INDEX_LIMIT_EXCEEDED => [
'name' => Exception::INDEX_LIMIT_EXCEEDED,
'description' => 'The maximum number of indexes has been reached.',
'description' => 'The maximum number of indexes for collection \'%s\' has been reached.',
'code' => 400,
],
Exception::INDEX_ALREADY_EXISTS => [
'name' => Exception::INDEX_ALREADY_EXISTS,
'description' => 'Index with the requested key already exists. Try again with a different key.',
'description' => 'Index with the requested key \'%s\' already exists. Try again with a different key.',
'code' => 409,
],
Exception::INDEX_INVALID => [
@@ -960,24 +965,24 @@ return [
],
Exception::INDEX_DEPENDENCY => [
'name' => Exception::INDEX_DEPENDENCY,
'description' => 'Attribute cannot be renamed or deleted. Please remove the associated index first.',
'description' => 'Attribute \'%s\' cannot be renamed or deleted. Please remove the associated index first.',
'code' => 409,
],
/** Column Indexes, same as Indexes but with different type */
Exception::COLUMN_INDEX_NOT_FOUND => [
'name' => Exception::COLUMN_INDEX_NOT_FOUND,
'description' => 'Index with the requested ID could not be found.',
'description' => 'Index with the requested key \'%s\' could not be found.',
'code' => 404,
],
Exception::COLUMN_INDEX_LIMIT_EXCEEDED => [
'name' => Exception::COLUMN_INDEX_LIMIT_EXCEEDED,
'description' => 'The maximum number of indexes has been reached.',
'description' => 'The maximum number of indexes for table \'%s\' has been reached.',
'code' => 400,
],
Exception::COLUMN_INDEX_ALREADY_EXISTS => [
'name' => Exception::COLUMN_INDEX_ALREADY_EXISTS,
'description' => 'Index with the requested key already exists. Try again with a different key.',
'description' => 'Index with the requested key \'%s\' already exists. Try again with a different key.',
'code' => 409,
],
Exception::COLUMN_INDEX_INVALID => [
@@ -987,19 +992,19 @@ return [
],
Exception::COLUMN_INDEX_DEPENDENCY => [
'name' => Exception::COLUMN_INDEX_DEPENDENCY,
'description' => 'Column cannot be renamed or deleted. Please remove the associated index first.',
'description' => 'Column \'%s\' cannot be renamed or deleted. Please remove the associated index first.',
'code' => 409,
],
/** Transactions */
Exception::TRANSACTION_NOT_FOUND => [
'name' => Exception::TRANSACTION_NOT_FOUND,
'description' => 'Transaction with the requested ID could not be found.',
'description' => 'Transaction with the requested ID \'%s\' could not be found.',
'code' => 404,
],
Exception::TRANSACTION_ALREADY_EXISTS => [
'name' => Exception::TRANSACTION_ALREADY_EXISTS,
'description' => 'Transaction with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'description' => 'Transaction with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::TRANSACTION_INVALID => [
@@ -1074,6 +1079,11 @@ return [
'description' => 'The project key has expired. Please generate a new key using the Appwrite console.',
'code' => 401,
],
Exception::ACCOUNT_KEY_EXPIRED => [
'name' => Exception::ACCOUNT_KEY_EXPIRED,
'description' => 'The account API key has expired. Please generate a new key using the Appwrite console.',
'code' => 401,
],
Exception::ROUTER_HOST_NOT_FOUND => [
'name' => Exception::ROUTER_HOST_NOT_FOUND,
'description' => 'Host is not trusted. This could occur because you have not configured a custom domain. Add a custom domain to your project first and try again.',
@@ -1103,7 +1113,6 @@ return [
'name' => Exception::RULE_VERIFICATION_FAILED,
'description' => 'Domain verification failed. Please check if your DNS records are correct and try again.',
'code' => 400,
'publish' => true
],
Exception::PROJECT_SMTP_CONFIG_INVALID => [
'name' => Exception::PROJECT_SMTP_CONFIG_INVALID,
@@ -1329,4 +1338,19 @@ return [
'description' => 'Target has an invalid provider type.',
'code' => 400,
],
Exception::USER_ID_MISSING => [
'name' => Exception::USER_ID_MISSING,
'description' => 'When using account API key, make sure to pass x-appwrite-user header with your user ID.',
'code' => 403,
],
Exception::ORGANIZATION_ID_MISSING => [
'name' => Exception::ORGANIZATION_ID_MISSING,
'description' => 'When using organization API key, make sure to pass x-appwrite-organization header with your organization ID.',
'code' => 403,
],
Exception::PROJECT_ID_MISSING => [
'name' => Exception::PROJECT_ID_MISSING,
'description' => 'When using project API key, make sure to pass x-appwrite-project header with your project ID.',
'code' => 403,
],
];
@@ -187,7 +187,7 @@
<img
height="26px"
src="{{logoUrl}}"
alt="Appwrite logo"
alt="{{platform}} logo"
/>
</td>
</tr>
@@ -225,7 +225,7 @@
<tr>
<td style="padding-left: 4px; padding-right: 4px">
<a
href="{{twitterUrl}}"
href="{{twitter}}"
class="social-icon"
title="Twitter"
>
@@ -234,7 +234,7 @@
</td>
<td style="padding-left: 4px; padding-right: 4px">
<a
href="{{discordUrl}}"
href="{{discord}}"
class="social-icon"
>
<img src="https://cloud.appwrite.io/images/mails/discord.png" height="24" width="24" />
@@ -242,7 +242,7 @@
</td>
<td style="padding-left: 4px; padding-right: 4px">
<a
href="{{githubUrl}}"
href="{{github}}"
class="social-icon"
>
<img src="https://cloud.appwrite.io/images/mails/github.png" height="24" width="24" />
@@ -252,15 +252,15 @@
</table>
<table style="width: auto; margin: 0 auto; margin-top: 60px">
<tr>
<td><a href="{{termsUrl}}">Terms</a></td>
<td><a href="{{terms}}">Terms</a></td>
<td style="color: #e8e9f0">
<div style="margin: 0 8px">|</div>
</td>
<td><a href="{{privacyUrl}}">Privacy</a></td>
<td><a href="{{privacy}}">Privacy</a></td>
</tr>
</table>
<p style="text-align: center" align="center">
&copy; {{year}} Appwrite | 251 Little Falls Drive, Wilmington 19808,
&copy; {{year}} {{platform}} | 251 Little Falls Drive, Wilmington 19808,
Delaware, United States
</p>
</div>
+21 -38
View File
@@ -50,17 +50,12 @@
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Poppins';
src: url('https://assets.appwrite.io/fonts/poppins/poppins-v23-latin-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
</style>
<style>
body {
.main {
max-width: 650px;
margin: 0 auto;
margin-top: 32px;
padding: 32px;
line-height: 1.5;
color: #616b7c;
@@ -68,15 +63,12 @@
font-weight: 400;
font-family: "Inter", sans-serif;
background-color: #ffffff;
margin: 0;
padding: 0;
line-height: 150%;
}
a {
.main a {
color: currentColor;
word-break: break-all;
}
a.button {
.main a.button {
box-sizing: border-box;
display: inline-block;
text-align: center;
@@ -87,8 +79,8 @@
border: 1px solid #414146;
border-radius: 8px;
}
a.button:hover,
a.button:focus {
.main a.button:hover,
.main a.button:focus {
opacity: 0.8;
}
table {
@@ -102,11 +94,6 @@
td {
vertical-align: top;
}
.main {
max-width: 650px;
margin: 0 auto;
margin-top: 32px;
}
h1 {
font-size: 22px;
margin-bottom: 0px;
@@ -138,9 +125,6 @@
border: none;
border-top: 1px solid #e8e9f0;
}
h* {
font-family: 'Poppins', sans-serif;
}
p {
margin-bottom: 10px;
}
@@ -155,21 +139,20 @@
<body style="direction: {{direction}}">
<div style="display: none; overflow: hidden; max-height: 0; max-width: 0; opacity: 0; line-height: 1px;">
{{preview}}
<div>{{previewWhitespace}}</div>
</div>
<div style="display: none; overflow: hidden; max-height: 0; max-width: 0; opacity: 0; line-height: 1px;">
{{preview}}
<div>{{previewWhitespace}}</div>
</div>
<div style="max-width:650px; word-wrap: break-word; overflow-wrap: break-word;
word-break: normal; margin:0 auto;">
<table style="margin-top: 32px">
<tr>
<td>
{{body}}
</td>
</tr>
</table>
</div>
<div class="main" style="max-width:650px; word-wrap: break-word; overflow-wrap: break-word; word-break: normal; margin:0 auto;">
<table style="margin-top: 32px">
<tr>
<td>
{{body}}
</td>
</tr>
</table>
</div>
</body>
</html>
+11
View File
@@ -462,4 +462,15 @@ return [
'mock' => true,
'class' => 'Appwrite\\Auth\\OAuth2\\Mock',
],
'mock-unverified' => [
'name' => 'MockUnverified',
'developers' => 'https://appwrite.io',
'icon' => 'icon-appwrite',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => true,
'class' => 'Appwrite\\Auth\\OAuth2\\MockUnverified',
],
];
+39
View File
@@ -0,0 +1,39 @@
<?php
use Utopia\System\System;
// For now, take first domain as primary (for previews)
// Later-on this can become platform-specific with new env var (appwrite=this,imagine=that)
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
if (\str_contains($sitesDomain, ',')) {
$sitesDomain = explode(',', $sitesDomain)[0];
}
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
if (\str_contains($functionsDomain, ',')) {
$functionsDomain = explode(',', $functionsDomain)[0];
}
/**
* Platform configuration
*/
return [
'apiHostname' => System::getEnv('_APP_DOMAIN', 'localhost'),
'consoleHostname' => System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', 'localhost')),
'hostnames' => array_filter(array_unique([
System::getEnv('_APP_DOMAIN', 'localhost'),
System::getEnv('_APP_CONSOLE_DOMAIN', 'localhost'),
])),
'platformName' => APP_EMAIL_PLATFORM_NAME,
'logoUrl' => APP_EMAIL_LOGO_URL,
'accentColor' => APP_EMAIL_ACCENT_COLOR,
'footerImageUrl' => APP_EMAIL_FOOTER_IMAGE_URL,
'twitterUrl' => APP_SOCIAL_TWITTER,
'discordUrl' => APP_SOCIAL_DISCORD,
'githubUrl' => APP_SOCIAL_GITHUB,
'termsUrl' => APP_EMAIL_TERMS_URL,
'privacyUrl' => APP_EMAIL_PRIVACY_URL,
'websiteUrl' => 'https://' . APP_DOMAIN,
'emailSenderName' => APP_EMAIL_PLATFORM_NAME,
'sitesDomain' => $sitesDomain,
'functionsDomain' => $functionsDomain,
];
+2
View File
@@ -58,6 +58,8 @@ $admins = [
'projects.write',
'keys.read',
'keys.write',
'devKeys.read',
'devKeys.write',
'webhooks.read',
'webhooks.write',
'locale.read',
+15
View File
@@ -0,0 +1,15 @@
<?php
// List of scopes for Account API keys (Tokens)
return [
"account" => [
"description" => 'Access to manage account, its organizations, sessions, tokens, and billing.',
],
"teams.read" => [
"description" => 'Access to read account\'s organizations.',
],
"teams.write" => [
"description" => 'Access to create, update and delete account\'s organizations and its memberships.',
],
];
+42
View File
@@ -0,0 +1,42 @@
<?php
// List of scopes for organization (teams) API keys
return [
"platforms.read" => [
"description" => 'Access to read project\'s platforms',
],
"platforms.write" => [
"description" =>
'Access to create, update, and delete project\'s platforms',
],
"projects.read" => [
"description" => 'Access to read organization\'s projects',
],
"projects.write" => [
"description" =>
"Access to create, update, and delete projects in organization",
],
"keys.read" => [
"description" => 'Access to read project\'s API keys',
],
"keys.write" => [
"description" =>
"Access to create, update, and delete project\'s API keys",
],
"devKeys.read" => [
"description" => 'Access to read project\'s development keys',
],
"devKeys.write" => [
"description" =>
"Access to create, update, and delete project\'s development keys",
],
"webhooks.read" => [
"description" =>
"Access to read project\'s webhooks",
],
"webhooks.write" => [
"description" =>
"Access to create, update, and delete project\'s webhooks",
],
];
@@ -1,8 +1,8 @@
<?php
return [
APP_PLATFORM_CLIENT => [
'key' => APP_PLATFORM_CLIENT,
APP_SDK_PLATFORM_CLIENT => [
'key' => APP_SDK_PLATFORM_CLIENT,
'name' => 'Client',
'description' => 'Client libraries for integrating with Appwrite to build client-based applications and websites. Read the [getting started for web](https://appwrite.io/docs/getting-started-for-web) or [getting started for Flutter](https://appwrite.io/docs/getting-started-for-flutter) tutorials to start building your first application.',
'enabled' => true,
@@ -11,14 +11,14 @@ return [
[
'key' => 'web',
'name' => 'Web',
'version' => '21.5.0',
'version' => '22.0.0',
'url' => 'https://github.com/appwrite/sdk-for-web',
'package' => 'https://www.npmjs.com/package/appwrite',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'family' => APP_PLATFORM_CLIENT,
'family' => APP_SDK_PLATFORM_CLIENT,
'prism' => 'javascript',
'source' => \realpath(__DIR__ . '/../sdks/client-web'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-web.git',
@@ -60,14 +60,14 @@ return [
[
'key' => 'flutter',
'name' => 'Flutter',
'version' => '20.3.2',
'version' => '21.0.0',
'url' => 'https://github.com/appwrite/sdk-for-flutter',
'package' => 'https://pub.dev/packages/appwrite',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'family' => APP_PLATFORM_CLIENT,
'family' => APP_SDK_PLATFORM_CLIENT,
'prism' => 'dart',
'source' => \realpath(__DIR__ . '/../sdks/client-flutter'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-flutter.git',
@@ -79,14 +79,14 @@ return [
[
'key' => 'apple',
'name' => 'Apple',
'version' => '13.5.0',
'version' => '14.0.0',
'url' => 'https://github.com/appwrite/sdk-for-apple',
'package' => 'https://github.com/appwrite/sdk-for-apple',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'family' => APP_PLATFORM_CLIENT,
'family' => APP_SDK_PLATFORM_CLIENT,
'prism' => 'swift',
'source' => \realpath(__DIR__ . '/../sdks/client-apple'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-apple.git',
@@ -104,7 +104,7 @@ return [
'beta' => false,
'dev' => false,
'hidden' => false,
'family' => APP_PLATFORM_CLIENT,
'family' => APP_SDK_PLATFORM_CLIENT,
'prism' => '',
'source' => false,
'gitUrl' => 'git@github.com:appwrite/sdk-for-objective-c.git',
@@ -116,14 +116,15 @@ return [
[
'key' => 'android',
'name' => 'Android',
'version' => '11.4.0',
'namespace' => 'io.appwrite',
'version' => '12.0.0',
'url' => 'https://github.com/appwrite/sdk-for-android',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-android',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'family' => APP_PLATFORM_CLIENT,
'family' => APP_SDK_PLATFORM_CLIENT,
'prism' => 'kotlin',
'source' => \realpath(__DIR__ . '/../sdks/client-android'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-android.git',
@@ -139,14 +140,14 @@ return [
[
'key' => 'react-native',
'name' => 'React Native',
'version' => '0.19.0',
'version' => '0.20.0',
'url' => 'https://github.com/appwrite/sdk-for-react-native',
'package' => 'https://npmjs.com/package/react-native-appwrite',
'enabled' => true,
'beta' => true,
'dev' => false,
'hidden' => false,
'family' => APP_PLATFORM_CLIENT,
'family' => APP_SDK_PLATFORM_CLIENT,
'prism' => 'javascript',
'source' => \realpath(__DIR__ . '/../sdks/client-react-native'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-react-native.git',
@@ -165,7 +166,7 @@ return [
'beta' => false,
'dev' => false,
'hidden' => true,
'family' => APP_PLATFORM_CLIENT,
'family' => APP_SDK_PLATFORM_CLIENT,
'prism' => 'graphql',
'source' => \realpath(__DIR__ . '/../sdks/client-graphql'),
'gitUrl' => '',
@@ -185,7 +186,7 @@ return [
'beta' => false,
'dev' => false,
'hidden' => true,
'family' => APP_PLATFORM_CLIENT,
'family' => APP_SDK_PLATFORM_CLIENT,
'prism' => 'http',
'source' => \realpath(__DIR__ . '/../sdks/client-rest'),
'gitUrl' => '',
@@ -198,8 +199,8 @@ return [
],
],
APP_PLATFORM_CONSOLE => [
'key' => APP_PLATFORM_CONSOLE,
APP_SDK_PLATFORM_CONSOLE => [
'key' => APP_SDK_PLATFORM_CONSOLE,
'name' => 'Console',
'enabled' => false,
'beta' => false,
@@ -207,14 +208,14 @@ return [
[
'key' => 'web',
'name' => 'Console',
'version' => '0.2.0',
'version' => '0.3.0',
'url' => '',
'package' => '',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => true,
'family' => APP_PLATFORM_CONSOLE,
'family' => APP_SDK_PLATFORM_CONSOLE,
'prism' => 'javascript',
'source' => \realpath(__DIR__ . '/../sdks/console-web'),
'gitUrl' => '',
@@ -226,14 +227,14 @@ return [
[
'key' => 'cli',
'name' => 'Command Line',
'version' => '12.0.1',
'version' => '13.2.0',
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
'beta' => true,
'dev' => false,
'hidden' => false,
'family' => APP_PLATFORM_CONSOLE,
'family' => APP_SDK_PLATFORM_CONSOLE,
'prism' => 'bash',
'source' => \realpath(__DIR__ . '/../sdks/console-cli'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-cli.git',
@@ -249,11 +250,31 @@ return [
],
],
],
[
'key' => 'markdown',
'name' => 'Markdown',
'version' => '0.2.0',
'url' => 'https://github.com/appwrite/sdk-for-md.git',
'package' => 'https://www.npmjs.com/package/@appwrite.io/docs',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'family' => APP_SDK_PLATFORM_CONSOLE,
'prism' => 'markdown',
'source' => \realpath(__DIR__ . '/../sdks/console-md'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-md.git',
'gitRepoName' => 'sdk-for-md',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'repoBranch' => 'main',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/md/CHANGELOG.md'),
],
],
],
APP_PLATFORM_SERVER => [
'key' => APP_PLATFORM_SERVER,
APP_SDK_PLATFORM_SERVER => [
'key' => APP_SDK_PLATFORM_SERVER,
'name' => 'Server',
'description' => 'Libraries for integrating with Appwrite to build server side integrations. Read the [getting started for server](https://appwrite.io/docs/getting-started-for-server) tutorial to start building your first server integration.',
'enabled' => true,
@@ -262,14 +283,14 @@ return [
[
'key' => 'nodejs',
'name' => 'Node.js',
'version' => '21.0.0',
'version' => '22.0.0',
'url' => 'https://github.com/appwrite/sdk-for-node',
'package' => 'https://www.npmjs.com/package/node-appwrite',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'family' => APP_PLATFORM_SERVER,
'family' => APP_SDK_PLATFORM_SERVER,
'prism' => 'javascript',
'source' => \realpath(__DIR__ . '/../sdks/server-nodejs'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-node.git',
@@ -281,14 +302,14 @@ return [
[
'key' => 'php',
'name' => 'PHP',
'version' => '19.0.0',
'version' => '20.0.0',
'url' => 'https://github.com/appwrite/sdk-for-php',
'package' => 'https://packagist.org/packages/appwrite/appwrite',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'family' => APP_PLATFORM_SERVER,
'family' => APP_SDK_PLATFORM_SERVER,
'prism' => 'php',
'source' => \realpath(__DIR__ . '/../sdks/server-php'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-php.git',
@@ -300,14 +321,14 @@ return [
[
'key' => 'python',
'name' => 'Python',
'version' => '14.0.0',
'version' => '15.0.0',
'url' => 'https://github.com/appwrite/sdk-for-python',
'package' => 'https://pypi.org/project/appwrite/',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'family' => APP_PLATFORM_SERVER,
'family' => APP_SDK_PLATFORM_SERVER,
'prism' => 'python',
'source' => \realpath(__DIR__ . '/../sdks/server-python'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-python.git',
@@ -319,14 +340,14 @@ return [
[
'key' => 'ruby',
'name' => 'Ruby',
'version' => '20.0.0',
'version' => '21.0.0',
'url' => 'https://github.com/appwrite/sdk-for-ruby',
'package' => 'https://rubygems.org/gems/appwrite',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'family' => APP_PLATFORM_SERVER,
'family' => APP_SDK_PLATFORM_SERVER,
'prism' => 'ruby',
'source' => \realpath(__DIR__ . '/../sdks/server-ruby'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-ruby.git',
@@ -338,14 +359,14 @@ return [
[
'key' => 'go',
'name' => 'Go',
'version' => 'v0.15.0',
'version' => 'v0.16.0',
'url' => 'https://github.com/appwrite/sdk-for-go',
'package' => 'https://github.com/appwrite/sdk-for-go',
'enabled' => true,
'beta' => true,
'dev' => false,
'hidden' => false,
'family' => APP_PLATFORM_SERVER,
'family' => APP_SDK_PLATFORM_SERVER,
'prism' => 'go',
'source' => \realpath(__DIR__ . '/../sdks/server-go'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-go.git',
@@ -357,14 +378,14 @@ return [
[
'key' => 'dotnet',
'name' => '.NET',
'version' => '0.23.0',
'version' => '0.25.0',
'url' => 'https://github.com/appwrite/sdk-for-dotnet',
'package' => 'https://www.nuget.org/packages/Appwrite',
'enabled' => true,
'beta' => true,
'dev' => false,
'hidden' => false,
'family' => APP_PLATFORM_SERVER,
'family' => APP_SDK_PLATFORM_SERVER,
'prism' => 'csharp',
'source' => \realpath(__DIR__ . '/../sdks/server-dotnet'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-dotnet.git',
@@ -376,14 +397,14 @@ return [
[
'key' => 'dart',
'name' => 'Dart',
'version' => '20.0.0',
'version' => '21.0.0',
'url' => 'https://github.com/appwrite/sdk-for-dart',
'package' => 'https://pub.dev/packages/dart_appwrite',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'family' => APP_PLATFORM_SERVER,
'family' => APP_SDK_PLATFORM_SERVER,
'prism' => 'dart',
'source' => \realpath(__DIR__ . '/../sdks/server-dart'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-dart.git',
@@ -395,14 +416,15 @@ return [
[
'key' => 'kotlin',
'name' => 'Kotlin',
'version' => '13.0.0',
'namespace' => 'io.appwrite',
'version' => '14.0.0',
'url' => 'https://github.com/appwrite/sdk-for-kotlin',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-kotlin',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'family' => APP_PLATFORM_SERVER,
'family' => APP_SDK_PLATFORM_SERVER,
'prism' => 'kotlin',
'source' => \realpath(__DIR__ . '/../sdks/server-kotlin'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-kotlin.git',
@@ -418,14 +440,14 @@ return [
[
'key' => 'swift',
'name' => 'Swift',
'version' => '14.0.0',
'version' => '15.0.0',
'url' => 'https://github.com/appwrite/sdk-for-swift',
'package' => 'https://github.com/appwrite/sdk-for-swift',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'family' => APP_PLATFORM_SERVER,
'family' => APP_SDK_PLATFORM_SERVER,
'prism' => 'swift',
'source' => \realpath(__DIR__ . '/../sdks/server-swift'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-swift.git',
@@ -444,7 +466,7 @@ return [
'beta' => false,
'dev' => false,
'hidden' => true,
'family' => APP_PLATFORM_SERVER,
'family' => APP_SDK_PLATFORM_SERVER,
'prism' => 'graphql',
'source' => \realpath(__DIR__ . '/../sdks/server-graphql'),
'gitUrl' => '',
@@ -464,7 +486,7 @@ return [
'beta' => false,
'dev' => false,
'hidden' => true,
'family' => APP_PLATFORM_SERVER,
'family' => APP_SDK_PLATFORM_SERVER,
'prism' => 'http',
'source' => \realpath(__DIR__ . '/../sdks/server-rest'),
'gitUrl' => '',
+28 -6
View File
@@ -13,19 +13,21 @@ return [
'tests' => false,
'optional' => false,
'icon' => '',
'platforms' => ['client', 'server', 'console'],
],
'web/console' => [
'key' => 'web/console',
'name' => 'Console',
'subtitle' => '',
'description' => '',
'controller' => 'web/console.php',
'controller' => '', // Uses modules
'sdk' => false,
'docs' => false,
'docsUrl' => '',
'tests' => false,
'optional' => false,
'icon' => '',
'platforms' => ['client', 'server', 'console'],
],
'account' => [
'key' => 'account',
@@ -39,19 +41,21 @@ return [
'tests' => false,
'optional' => true,
'icon' => '/images/services/account.png',
'platforms' => ['client', 'server', 'console'],
],
'avatars' => [
'key' => 'avatars',
'name' => 'Avatars',
'subtitle' => 'The Avatars service aims to help you complete everyday tasks related to your app image, icons, and avatars.',
'description' => '/docs/services/avatars.md',
'controller' => 'api/avatars.php',
'controller' => '', // Uses modules
'sdk' => true,
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/client/avatars',
'tests' => false,
'optional' => true,
'icon' => '/images/services/avatars.png',
'platforms' => ['client', 'server', 'console'],
],
'databases' => [
'key' => 'databases',
@@ -65,6 +69,7 @@ return [
'tests' => false,
'optional' => true,
'icon' => '/images/services/databases.png',
'platforms' => ['client', 'server', 'console'],
],
'tablesdb' => [
'key' => 'tablesdb',
@@ -78,6 +83,7 @@ return [
'tests' => false,
'optional' => true,
'icon' => '/images/services/databases.png',
'platforms' => ['client', 'server', 'console'],
],
'locale' => [
'key' => 'locale',
@@ -91,19 +97,21 @@ return [
'tests' => false,
'optional' => true,
'icon' => '/images/services/locale.png',
'platforms' => ['client', 'server', 'console'],
],
'health' => [
'key' => 'health',
'name' => 'Health',
'subtitle' => 'The Health service allows you to both validate and monitor your Appwrite server\'s health.',
'description' => '/docs/services/health.md',
'controller' => 'api/health.php',
'controller' => '', // Uses modules
'sdk' => true,
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/server/health',
'tests' => false,
'optional' => true,
'icon' => '/images/services/health.png',
'platforms' => ['client', 'server', 'console'],
],
'projects' => [
'key' => 'projects',
@@ -117,6 +125,7 @@ return [
'tests' => false,
'optional' => false,
'icon' => '',
'platforms' => ['client', 'server', 'console'],
],
'project' => [
'key' => 'project',
@@ -130,19 +139,21 @@ return [
'tests' => false,
'optional' => false,
'icon' => '',
'platforms' => ['client', 'server', 'console'],
],
'storage' => [
'key' => 'storage',
'name' => 'Storage',
'subtitle' => 'The Storage service allows you to manage your project files.',
'description' => '/docs/services/storage.md',
'controller' => 'api/storage.php',
'controller' => '', // Uses modules
'sdk' => true,
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/client/storage',
'tests' => false,
'optional' => true,
'icon' => '/images/services/storage.png',
'platforms' => ['client', 'server', 'console'],
],
'teams' => [
'key' => 'teams',
@@ -156,6 +167,7 @@ return [
'tests' => false,
'optional' => true,
'icon' => '/images/services/teams.png',
'platforms' => ['client', 'server', 'console'],
],
'users' => [
'key' => 'users',
@@ -169,6 +181,7 @@ return [
'tests' => false,
'optional' => true,
'icon' => '/images/services/users.png',
'platforms' => ['client', 'server', 'console'],
],
'vcs' => [
'key' => 'vcs',
@@ -182,6 +195,7 @@ return [
'tests' => false,
'optional' => false,
'icon' => '',
'platforms' => ['client', 'server', 'console'],
],
'sites' => [
'key' => 'sites',
@@ -195,6 +209,7 @@ return [
'tests' => false,
'optional' => true,
'icon' => '/images/services/sites.png',
'platforms' => ['client', 'server', 'console'],
],
'functions' => [
'key' => 'functions',
@@ -208,6 +223,7 @@ return [
'tests' => false,
'optional' => true,
'icon' => '/images/services/functions.png',
'platforms' => ['client', 'server', 'console'],
],
'proxy' => [
'key' => 'proxy',
@@ -221,6 +237,7 @@ return [
'tests' => false,
'optional' => false,
'icon' => '/images/services/proxy.png',
'platforms' => ['client', 'server', 'console'],
],
'mock' => [
'key' => 'mock',
@@ -234,6 +251,7 @@ return [
'tests' => true,
'optional' => false,
'icon' => '',
'platforms' => ['client', 'server', 'console'],
],
'graphql' => [
'key' => 'graphql',
@@ -247,19 +265,21 @@ return [
'tests' => true,
'optional' => true,
'icon' => '/images/services/graphql.png',
'platforms' => ['client', 'server', 'console'],
],
'console' => [
'key' => 'console',
'name' => 'Console',
'subtitle' => 'The Console service allows you to interact with console relevant informations.',
'subtitle' => 'The Console service allows you to interact with console relevant information.',
'description' => '',
'controller' => 'api/console.php',
'controller' => '', // Uses modules
'sdk' => true,
'docs' => true,
'docsUrl' => '',
'tests' => false,
'optional' => false,
'icon' => '',
'platforms' => ['client', 'server', 'console'],
],
'migrations' => [
'key' => 'migrations',
@@ -273,6 +293,7 @@ return [
'tests' => true,
'optional' => false,
'icon' => '/images/services/migrations.png',
'platforms' => ['client', 'server', 'console'],
],
'messaging' => [
'key' => 'messaging',
@@ -286,5 +307,6 @@ return [
'tests' => true,
'optional' => true,
'icon' => '/images/services/messaging.png',
'platforms' => ['client', 'server', 'console'],
]
];
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -3,4 +3,6 @@
use Utopia\Image\Image;
use Utopia\System\System;
Image::setResourceLimit('memory', intval(System::getEnv('_APP_IMAGES_RESOURCE_LIMIT_MEMORY', 1024*1024*64)));
if (\class_exists('Imagick')) {
Image::setResourceLimit('memory', intval(System::getEnv('_APP_IMAGES_RESOURCE_LIMIT_MEMORY', 1024*1024*64)));
}
+68 -40
View File
@@ -20,6 +20,34 @@ function getRuntimes($runtimes, $commands, $entrypoint, $providerRootDirectory,
}));
}
class FunctionUseCases
{
public const STARTER = 'starter';
public const DATABASES = 'databases';
public const AI = 'ai';
public const MESSAGING = 'messaging';
public const UTILITIES = 'utilities';
public const DEV_TOOLS = 'dev-tools';
public const AUTH = 'auth';
/**
* @var array<string>
*/
public static function getAll(): array
{
return [
self::STARTER,
self::DATABASES,
self::AI,
self::MESSAGING,
self::UTILITIES,
self::DEV_TOOLS,
self::AUTH,
];
}
}
return [
[
'icon' => 'icon-lightning-bolt',
@@ -32,7 +60,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['starter'],
'useCases' => [FunctionUseCases::STARTER],
'runtimes' => [
...getRuntimes($templateRuntimes['NODE'], 'npm install', 'src/main.js', 'node/starter', $allowList),
...getRuntimes(
@@ -73,7 +101,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['databases'],
'useCases' => [FunctionUseCases::DATABASES],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -119,7 +147,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['databases'],
'useCases' => [FunctionUseCases::DATABASES],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -164,7 +192,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['databases'],
'useCases' => [FunctionUseCases::DATABASES],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -218,7 +246,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['databases'],
'useCases' => [FunctionUseCases::DATABASES],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -257,7 +285,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['databases'],
'useCases' => [FunctionUseCases::DATABASES],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -326,7 +354,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['ai'],
'useCases' => [FunctionUseCases::AI],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -392,7 +420,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['messaging'],
'useCases' => [FunctionUseCases::MESSAGING],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -459,7 +487,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['ai'],
'useCases' => [FunctionUseCases::AI],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -497,7 +525,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['ai'],
'useCases' => [FunctionUseCases::AI],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -548,7 +576,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['utilities'],
'useCases' => [FunctionUseCases::UTILITIES],
'runtimes' => [
...getRuntimes($templateRuntimes['NODE'], 'npm install', 'src/main.js', 'node/generate-pdf', $allowList)
],
@@ -571,7 +599,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['dev-tools'],
'useCases' => [FunctionUseCases::DEV_TOOLS],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -616,7 +644,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['utilities'],
'useCases' => [FunctionUseCases::UTILITIES],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -669,7 +697,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['databases'],
'useCases' => [FunctionUseCases::DATABASES],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -754,7 +782,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['databases'],
'useCases' => [FunctionUseCases::DATABASES],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -853,7 +881,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['messaging'],
'useCases' => [FunctionUseCases::MESSAGING],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -946,7 +974,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['messaging'],
'useCases' => [FunctionUseCases::MESSAGING],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -1004,7 +1032,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['utilities'],
'useCases' => [FunctionUseCases::UTILITIES],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -1091,7 +1119,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['utilities'],
'useCases' => [FunctionUseCases::UTILITIES],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -1134,7 +1162,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['utilities'],
'useCases' => [FunctionUseCases::UTILITIES],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -1193,7 +1221,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 30,
'useCases' => ['ai'],
'useCases' => [FunctionUseCases::AI],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -1229,7 +1257,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 30,
'useCases' => ['ai'],
'useCases' => [FunctionUseCases::AI],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -1265,7 +1293,7 @@ return [
'events' => ['buckets.*.files.*.create'],
'cron' => '',
'timeout' => 15,
'useCases' => ['ai'],
'useCases' => [FunctionUseCases::AI],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -1325,7 +1353,7 @@ return [
'events' => ['buckets.*.files.*.create'],
'cron' => '',
'timeout' => 15,
'useCases' => ['ai'],
'useCases' => [FunctionUseCases::AI],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -1385,7 +1413,7 @@ return [
'events' => ['buckets.*.files.*.create'],
'cron' => '',
'timeout' => 15,
'useCases' => ['ai'],
'useCases' => [FunctionUseCases::AI],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -1448,7 +1476,7 @@ return [
],
'cron' => '',
'timeout' => 15,
'useCases' => ['ai'],
'useCases' => [FunctionUseCases::AI],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -1508,7 +1536,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 300,
'useCases' => ['ai'],
'useCases' => [FunctionUseCases::AI],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -1545,7 +1573,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 300,
'useCases' => ['ai'],
'useCases' => [FunctionUseCases::AI],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -1589,7 +1617,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['ai'],
'useCases' => [FunctionUseCases::AI],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -1632,7 +1660,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 300,
'useCases' => ['ai'],
'useCases' => [FunctionUseCases::AI],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -1669,7 +1697,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 30,
'useCases' => ['ai'],
'useCases' => [FunctionUseCases::AI],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -1734,7 +1762,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 30,
'useCases' => ['ai'],
'useCases' => [FunctionUseCases::AI],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -1799,7 +1827,7 @@ return [
'cron' => '',
'events' => [],
'timeout' => 15,
'useCases' => ['ai'],
'useCases' => [FunctionUseCases::AI],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -1856,7 +1884,7 @@ return [
'cron' => '',
'events' => [],
'timeout' => 15,
'useCases' => ['ai'],
'useCases' => [FunctionUseCases::AI],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -1899,7 +1927,7 @@ return [
'cron' => '',
'events' => [],
'timeout' => 15,
'useCases' => ['ai'],
'useCases' => [FunctionUseCases::AI],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -1942,7 +1970,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['ai'],
'useCases' => [FunctionUseCases::AI],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -1986,7 +2014,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 300,
'useCases' => ['ai'],
'useCases' => [FunctionUseCases::AI],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -2023,7 +2051,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['utilities'],
'useCases' => [FunctionUseCases::UTILITIES],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -2080,7 +2108,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['utilities'],
'useCases' => [FunctionUseCases::UTILITIES],
'runtimes' => [
...getRuntimes(
$templateRuntimes['NODE'],
@@ -2153,7 +2181,7 @@ return [
'events' => [],
'cron' => '',
'timeout' => 15,
'useCases' => ['auth'],
'useCases' => [FunctionUseCases::AUTH],
'runtimes' => [
...getRuntimes($templateRuntimes['DART'], 'dart pub get', 'lib/main.dart', 'dart/sign_in_with_apple', $allowList)
],
+230 -82
View File
@@ -1,5 +1,6 @@
<?php
use Utopia\Config\Config;
use Utopia\System\System;
/**
@@ -7,16 +8,12 @@ use Utopia\System\System;
*/
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_DOMAIN', '');
// Temporary fix until we can set _APP_DOMAIN to "localhost" instead of "traefik"
if (System::getEnv('_APP_ENV', 'development') === 'development') {
$hostname = 'localhost';
}
$platform = Config::getParam('platform', []);
$hostname = $platform['consoleHostname'] ?? '';
$url = $protocol . '://' . $hostname;
class UseCases
class SiteUseCases
{
public const PORTFOLIO = 'portfolio';
public const STARTER = 'starter';
@@ -24,7 +21,27 @@ class UseCases
public const ECOMMERCE = 'ecommerce';
public const DOCUMENTATION = 'documentation';
public const BLOG = 'blog';
public const AI = 'artificial intelligence';
public const AI = 'ai';
public const FORMS = 'forms';
public const DASHBOARD = 'dashboard';
/**
* @var array<string>
*/
public static function getAll(): array
{
return [
self::PORTFOLIO,
self::STARTER,
self::EVENTS,
self::ECOMMERCE,
self::DOCUMENTATION,
self::BLOG,
self::AI,
self::FORMS,
self::DASHBOARD,
];
}
}
const TEMPLATE_FRAMEWORKS = [
@@ -189,7 +206,7 @@ return [
'name' => 'Documentation template',
'tagline' => 'Modern site to store your knowledge with a clean design, full-text search, dark mode, and more.',
'score' => 6, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::DOCUMENTATION],
'useCases' => [SiteUseCases::DOCUMENTATION],
'screenshotDark' => $url . '/images/sites/templates/template-for-documentation-dark.png',
'screenshotLight' => $url . '/images/sites/templates/template-for-documentation-light.png',
'frameworks' => [
@@ -210,7 +227,7 @@ return [
// When we add Lynx with Appwrite SDK, use following tagline for it:
// 'tagline' => 'Sample application built with Lynx, a cross-platform framework focused on performance.',
'score' => 1, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'screenshotDark' => $url . '/images/sites/templates/playground-for-lynx-dark.png',
'screenshotLight' => $url . '/images/sites/templates/playground-for-lynx-light.png',
'frameworks' => [
@@ -221,7 +238,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => []
],
[
@@ -229,7 +246,7 @@ return [
'name' => 'Vitepress',
'tagline' => 'Platform for documentation and knowledge sharing powered by Vite.',
'score' => 6, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::DOCUMENTATION],
'useCases' => [SiteUseCases::DOCUMENTATION],
'screenshotDark' => $url . '/images/sites/templates/vitepress-dark.png',
'screenshotLight' => $url . '/images/sites/templates/vitepress-light.png',
'frameworks' => [
@@ -244,7 +261,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => []
],
[
@@ -252,7 +269,7 @@ return [
'name' => 'Vuepress',
'tagline' => 'Platform for documentation and knowledge sharing powered by Vue.',
'score' => 4, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::DOCUMENTATION],
'useCases' => [SiteUseCases::DOCUMENTATION],
'screenshotDark' => $url . '/images/sites/templates/vuepress-dark.png',
'screenshotLight' => $url . '/images/sites/templates/vuepress-light.png',
'frameworks' => [
@@ -267,7 +284,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => []
],
[
@@ -275,7 +292,7 @@ return [
'name' => 'Docusaurus',
'tagline' => 'Platform for documentation and knowledge sharing powered by React.',
'score' => 4, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::DOCUMENTATION],
'useCases' => [SiteUseCases::DOCUMENTATION],
'screenshotDark' => $url . '/images/sites/templates/docusaurus-dark.png',
'screenshotLight' => $url . '/images/sites/templates/docusaurus-light.png',
'frameworks' => [
@@ -290,7 +307,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => []
],
[
@@ -298,7 +315,7 @@ return [
'name' => 'Nxt Lnk',
'tagline' => 'Personal website for creators to merge all URLs to social profiles.',
'score' => 6, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::PORTFOLIO],
'useCases' => [SiteUseCases::PORTFOLIO],
'screenshotDark' => $url . '/images/sites/templates/nxt-lnk-dark.png',
'screenshotLight' => $url . '/images/sites/templates/nxt-lnk-light.png',
'frameworks' => [
@@ -309,7 +326,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => []
],
[
@@ -317,7 +334,7 @@ return [
'name' => 'Magic Portfolio',
'tagline' => 'Complex personal website to showcase your projects, articles, and more.',
'score' => 7, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::PORTFOLIO],
'useCases' => [SiteUseCases::PORTFOLIO],
'screenshotDark' => $url . '/images/sites/templates/magic-portfolio-dark.png',
'screenshotLight' => $url . '/images/sites/templates/magic-portfolio-light.png',
'frameworks' => [
@@ -328,7 +345,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => []
],
[
@@ -336,7 +353,7 @@ return [
'name' => 'LittleLink',
'tagline' => 'Personal website for creators to merge all URLs to social profiles.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::PORTFOLIO],
'useCases' => [SiteUseCases::PORTFOLIO],
'screenshotDark' => $url . '/images/sites/templates/littlelink-dark.png',
'screenshotLight' => $url . '/images/sites/templates/littlelink-light.png',
'frameworks' => [
@@ -347,7 +364,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => []
],
[
@@ -355,7 +372,7 @@ return [
'name' => 'Logspot',
'tagline' => 'Website to publish changelogs of your application.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::BLOG],
'useCases' => [SiteUseCases::BLOG],
'screenshotDark' => $url . '/images/sites/templates/logspot-dark.png',
'screenshotLight' => $url . '/images/sites/templates/logspot-light.png',
'frameworks' => [
@@ -369,7 +386,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => []
],
[
@@ -377,7 +394,7 @@ return [
'name' => 'Astro Nano',
'tagline' => 'Minimal personal website to showcase your projects, articles, and more.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::PORTFOLIO],
'useCases' => [SiteUseCases::PORTFOLIO],
'screenshotDark' => $url . '/images/sites/templates/astro-nano-dark.png',
'screenshotLight' => $url . '/images/sites/templates/astro-nano-light.png',
'frameworks' => [
@@ -390,7 +407,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => []
],
[
@@ -398,7 +415,7 @@ return [
'name' => 'Astro Starlight',
'tagline' => 'Platform for documentation and knowledge sharing powered by Astro.',
'score' => 6, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::DOCUMENTATION],
'useCases' => [SiteUseCases::DOCUMENTATION],
'screenshotDark' => $url . '/images/sites/templates/astro-starlight-dark.png',
'screenshotLight' => $url . '/images/sites/templates/astro-starlight-light.png',
'frameworks' => [
@@ -411,7 +428,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => []
],
[
@@ -419,7 +436,7 @@ return [
'name' => 'Astro Sphere',
'tagline' => 'Modern personal website to showcase your projects, articles, and more.',
'score' => 7, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::PORTFOLIO],
'useCases' => [SiteUseCases::PORTFOLIO],
'screenshotDark' => $url . '/images/sites/templates/astro-sphere-dark.png',
'screenshotLight' => $url . '/images/sites/templates/astro-sphere-light.png',
'frameworks' => [
@@ -432,7 +449,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => []
],
[
@@ -440,7 +457,7 @@ return [
'name' => 'Astro Starlog',
'tagline' => 'Platform for publishing written content and media powered by Astro.',
'score' => 5, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::BLOG],
'useCases' => [SiteUseCases::BLOG],
'screenshotDark' => $url . '/images/sites/templates/astro-starlog-dark.png',
'screenshotLight' => $url . '/images/sites/templates/astro-starlog-light.png',
'frameworks' => [
@@ -453,7 +470,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => []
],
[
@@ -461,7 +478,7 @@ return [
'name' => 'Onelink',
'tagline' => 'Personal website for creators to merge all URLs to social profiles.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::PORTFOLIO],
'useCases' => [SiteUseCases::PORTFOLIO],
'screenshotDark' => $url . '/images/sites/templates/onelink-dark.png',
'screenshotLight' => $url . '/images/sites/templates/onelink-light.png',
'frameworks' => [
@@ -475,13 +492,13 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => []
],
[
'key' => 'starter-for-flutter',
'name' => 'Flutter starter',
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'tagline' => 'Simple Flutter application integrated with Appwrite SDK.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-flutter-dark.png',
@@ -526,7 +543,7 @@ return [
[
'key' => 'starter-for-js',
'name' => 'JavaScript starter',
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'tagline' => 'Simple JavaScript application integrated with Appwrite SDK.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-js-dark.png',
@@ -570,7 +587,7 @@ return [
[
'key' => 'starter-for-angular',
'name' => 'Angular starter',
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'tagline' => 'Simple Angular application integrated with Appwrite SDK.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-angular-dark.png',
@@ -616,7 +633,7 @@ return [
[
'key' => 'starter-for-astro',
'name' => 'Astro starter',
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'tagline' => 'Simple Astro application integrated with Appwrite SDK.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-astro-dark.png',
@@ -661,7 +678,7 @@ return [
[
'key' => 'starter-for-analog',
'name' => 'Analog starter',
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'tagline' => 'Simple Analog application integrated with Appwrite SDK.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-analog-dark.png',
@@ -705,7 +722,7 @@ return [
[
'key' => 'starter-for-remix',
'name' => 'Remix starter',
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'tagline' => 'Simple Remix application integrated with Appwrite SDK.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-remix-dark.png',
@@ -749,7 +766,7 @@ return [
[
'key' => 'starter-for-svelte',
'name' => 'Svelte starter',
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'tagline' => 'Simple Svelte application integrated with Appwrite SDK.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-svelte-dark.png',
@@ -793,7 +810,7 @@ return [
[
'key' => 'starter-for-react',
'name' => 'React starter',
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'tagline' => 'Simple React application integrated with Appwrite SDK.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-react-dark.png',
@@ -837,7 +854,7 @@ return [
[
'key' => 'starter-for-vue',
'name' => 'Vue starter',
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'tagline' => 'Simple Vue application integrated with Appwrite SDK.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-vue-dark.png',
@@ -881,7 +898,7 @@ return [
[
'key' => 'starter-for-react-native',
'name' => 'React Native starter',
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'tagline' => 'Simple React Native application integrated with Appwrite SDK.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-react-native-dark.png',
@@ -925,7 +942,7 @@ return [
[
'key' => 'starter-for-nextjs',
'name' => 'Next.js starter',
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'tagline' => 'Simple Next.js application integrated with Appwrite SDK.',
'score' => 6, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-nextjs-dark.png',
@@ -969,7 +986,7 @@ return [
[
'key' => 'starter-for-tanstack-start',
'name' => 'TanStack Start starter',
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'tagline' => 'Simple TanStack Start application integrated with Appwrite SDK.',
'score' => 9, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-tanstack-start-dark.png',
@@ -1013,7 +1030,7 @@ return [
[
'key' => 'starter-for-nuxt',
'name' => 'Nuxt starter',
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'tagline' => 'Simple Nuxt application integrated with Appwrite SDK.',
'score' => 3, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-nuxt-dark.png',
@@ -1059,7 +1076,7 @@ return [
'name' => 'Event template',
'tagline' => 'Hackathon landing page with support for project submissions.',
'score' => 6, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::EVENTS],
'useCases' => [SiteUseCases::EVENTS],
'screenshotDark' => $url . '/images/sites/templates/template-for-event-dark.png',
'screenshotLight' => $url . '/images/sites/templates/template-for-event-light.png',
'frameworks' => [
@@ -1097,7 +1114,7 @@ return [
'name' => 'Portfolio template',
'tagline' => 'Simple personal website to showcase your projects, articles, and more.',
'score' => 6, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::PORTFOLIO],
'useCases' => [SiteUseCases::PORTFOLIO],
'screenshotDark' => $url . '/images/sites/templates/template-for-portfolio-dark.png',
'screenshotLight' => $url . '/images/sites/templates/template-for-portfolio-light.png',
'frameworks' => [
@@ -1116,7 +1133,7 @@ return [
'name' => 'Store template',
'tagline' => 'E-commerce platform for selling products with Stripe integration.',
'score' => 7, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::ECOMMERCE],
'useCases' => [SiteUseCases::ECOMMERCE],
'screenshotDark' => $url . '/images/sites/templates/template-for-store-dark.png',
'screenshotLight' => $url . '/images/sites/templates/template-for-store-light.png',
'frameworks' => [
@@ -1160,7 +1177,7 @@ return [
'name' => 'Blog template',
'tagline' => 'Platform for publishing written content and media.',
'score' => 7, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::BLOG],
'useCases' => [SiteUseCases::BLOG],
'screenshotDark' => $url . '/images/sites/templates/template-for-blog-dark.png',
'screenshotLight' => $url . '/images/sites/templates/template-for-blog-light.png',
'frameworks' => [
@@ -1179,7 +1196,7 @@ return [
'name' => 'Astro playground',
'tagline' => 'A basic Astro website without Appwrite SDK integration.',
'score' => 1, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'screenshotDark' => $url . '/images/sites/templates/playground-for-astro-dark.png',
'screenshotLight' => $url . '/images/sites/templates/playground-for-astro-light.png',
'frameworks' => [
@@ -1190,7 +1207,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => [],
],
[
@@ -1198,7 +1215,7 @@ return [
'name' => 'Remix playground',
'tagline' => 'A basic Remix website without Appwrite SDK integration.',
'score' => 1, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'screenshotDark' => $url . '/images/sites/templates/playground-for-remix-dark.png',
'screenshotLight' => $url . '/images/sites/templates/playground-for-remix-light.png',
'frameworks' => [
@@ -1209,7 +1226,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => [],
],
[
@@ -1217,7 +1234,7 @@ return [
'name' => 'Next.js playground',
'tagline' => 'A basic Next.js website without Appwrite SDK integration.',
'score' => 1, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'screenshotDark' => $url . '/images/sites/templates/playground-for-nextjs-dark.png',
'screenshotLight' => $url . '/images/sites/templates/playground-for-nextjs-light.png',
'frameworks' => [
@@ -1228,7 +1245,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => [],
],
[
@@ -1236,7 +1253,7 @@ return [
'name' => 'Flutter playground',
'tagline' => 'A basic Flutter website without Appwrite SDK integration.',
'score' => 1, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'screenshotDark' => $url . '/images/sites/templates/playground-for-flutter-dark.png',
'screenshotLight' => $url . '/images/sites/templates/playground-for-flutter-light.png',
'frameworks' => [
@@ -1247,7 +1264,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => [],
],
[
@@ -1255,7 +1272,7 @@ return [
'name' => 'Vite playground',
'tagline' => 'A basic Vite website without Appwrite SDK integration.',
'score' => 1, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'screenshotDark' => $url . '/images/sites/templates/playground-for-vite-dark.png',
'screenshotLight' => $url . '/images/sites/templates/playground-for-vite-light.png',
'frameworks' => [
@@ -1266,7 +1283,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => [],
],
[
@@ -1274,7 +1291,7 @@ return [
'name' => 'Angular playground',
'tagline' => 'A basic Angular website without Appwrite SDK integration.',
'score' => 1, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'screenshotDark' => $url . '/images/sites/templates/playground-for-angular-dark.png',
'screenshotLight' => $url . '/images/sites/templates/playground-for-angular-light.png',
'frameworks' => [
@@ -1286,7 +1303,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => [],
],
[
@@ -1294,7 +1311,7 @@ return [
'name' => 'Analog playground',
'tagline' => 'A basic Analog website without Appwrite SDK integration.',
'score' => 1, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'screenshotDark' => $url . '/images/sites/templates/playground-for-analog-dark.png',
'screenshotLight' => $url . '/images/sites/templates/playground-for-analog-light.png',
'frameworks' => [
@@ -1305,7 +1322,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => [],
],
[
@@ -1313,7 +1330,7 @@ return [
'name' => 'Svelte playground',
'tagline' => 'A basic Svelte website without Appwrite SDK integration.',
'score' => 1, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'screenshotDark' => $url . '/images/sites/templates/playground-for-svelte-dark.png',
'screenshotLight' => $url . '/images/sites/templates/playground-for-svelte-light.png',
'frameworks' => [
@@ -1324,7 +1341,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => [],
],
@@ -1333,7 +1350,7 @@ return [
'name' => 'React playground',
'tagline' => 'A basic React website without Appwrite SDK integration.',
'score' => 1, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'screenshotDark' => $url . '/images/sites/templates/playground-for-react-dark.png',
'screenshotLight' => $url . '/images/sites/templates/playground-for-react-light.png',
'frameworks' => [
@@ -1345,7 +1362,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => [],
],
@@ -1354,7 +1371,7 @@ return [
'name' => 'Vue playground',
'tagline' => 'A basic Vue website without Appwrite SDK integration.',
'score' => 1, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'screenshotDark' => $url . '/images/sites/templates/playground-for-vue-dark.png',
'screenshotLight' => $url . '/images/sites/templates/playground-for-vue-light.png',
'frameworks' => [
@@ -1365,7 +1382,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => [],
],
[
@@ -1373,7 +1390,7 @@ return [
'name' => 'Nuxt playground',
'tagline' => 'A basic Nuxt website without Appwrite SDK integration.',
'score' => 1, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'screenshotDark' => $url . '/images/sites/templates/playground-for-nuxt-dark.png',
'screenshotLight' => $url . '/images/sites/templates/playground-for-nuxt-light.png',
'frameworks' => [
@@ -1384,7 +1401,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => [],
],
[
@@ -1392,7 +1409,7 @@ return [
'name' => 'TanStack Start playground',
'tagline' => 'A basic TanStack Start website without Appwrite SDK integration.',
'score' => 1, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'screenshotDark' => $url . '/images/sites/templates/playground-for-tanstack-start-dark.png',
'screenshotLight' => $url . '/images/sites/templates/playground-for-tanstack-start-light.png',
'frameworks' => [
@@ -1403,7 +1420,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.5.*',
'providerVersion' => '0.7.*',
'variables' => [],
],
[
@@ -1411,7 +1428,7 @@ return [
'name' => 'React Native playground',
'tagline' => 'A basic React Native website without Appwrite SDK integration.',
'score' => 1, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'screenshotDark' => $url . '/images/sites/templates/playground-for-react-native-dark.png',
'screenshotLight' => $url . '/images/sites/templates/playground-for-react-native-light.png',
'frameworks' => [
@@ -1422,7 +1439,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => [],
],
[
@@ -1430,7 +1447,7 @@ return [
'name' => 'Lynx gallery',
'tagline' => 'A Lynx website showcasing gallery with smooth animations.',
'score' => 1, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::STARTER],
'useCases' => [SiteUseCases::STARTER],
'screenshotDark' => $url . '/images/sites/templates/gallery-for-lynx-dark.png',
'screenshotLight' => $url . '/images/sites/templates/gallery-for-lynx-light.png',
'frameworks' => [
@@ -1441,7 +1458,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.3.*',
'providerVersion' => '0.7.*',
'variables' => []
],
[
@@ -1449,7 +1466,7 @@ return [
'name' => 'Text-to-speech with ElevenLabs',
'tagline' => 'Next.js app that transforms text into natural, human-like speech using ElevenLabs',
'score' => 10, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::AI],
'useCases' => [SiteUseCases::AI],
'screenshotDark' => $url . '/images/sites/templates/text-to-speech-dark.png',
'screenshotLight' => $url . '/images/sites/templates/text-to-speech-light.png',
'frameworks' => [
@@ -1460,7 +1477,7 @@ return [
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.6.*',
'providerVersion' => '0.7.*',
'variables' => [
[
'name' => 'ELEVENLABS_API_KEY',
@@ -1472,4 +1489,135 @@ return [
],
]
],
[
'key' => 'crm-dashboard-react-admin',
'name' => 'CRM dashboard with React Admin',
'tagline' => 'A React-based admin dashboard template with CRM features.',
'score' => 4, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [SiteUseCases::DASHBOARD],
'screenshotDark' => $url . '/images/sites/templates/crm-dashboard-react-admin-dark.png',
'screenshotLight' => $url . '/images/sites/templates/crm-dashboard-react-admin-light.png',
'frameworks' => [
getFramework('REACT', [
'providerRootDirectory' => './react/react-admin',
'installCommand' => 'pnpm install',
'buildCommand' => 'pnpm build && pnpm db-seed',
'outputDirectory' => './dist',
]),
],
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.7.*',
'variables' => [
[
'name' => 'VITE_APPWRITE_ENDPOINT',
'description' => 'Endpoint of Appwrite server',
'value' => '{apiEndpoint}',
'placeholder' => '{apiEndpoint}',
'required' => true,
'type' => 'text'
],
[
'name' => 'VITE_APPWRITE_PROJECT_ID',
'description' => 'Your Appwrite project ID',
'value' => '{projectId}',
'placeholder' => '{projectId}',
'required' => true,
'type' => 'text'
],
[
'name' => 'APPWRITE_API_KEY',
'description' => 'Your Appwrite API key (for seeding only)',
'value' => '',
'placeholder' => 'a0b1...',
'required' => true,
'type' => 'password'
],
[
'name' => 'VITE_APPWRITE_DATABASE_ID',
'description' => 'Database ID (default: admin)',
'value' => 'admin',
'placeholder' => 'admin',
'required' => false,
'type' => 'text'
],
[
'name' => 'VITE_APPWRITE_TABLE_REVIEWS',
'description' => 'Table ID for reviews table',
'value' => 'reviews',
'placeholder' => 'reviews',
'required' => false,
'type' => 'text'
],
[
'name' => 'VITE_APPWRITE_TABLE_INVOICES',
'description' => 'Table ID for invoices table',
'value' => 'invoices',
'placeholder' => 'invoices',
'required' => false,
'type' => 'text'
],
[
'name' => 'VITE_APPWRITE_TABLE_ORDERS',
'description' => 'Table ID for orders table',
'value' => 'orders',
'placeholder' => 'orders',
'required' => false,
'type' => 'text'
],
[
'name' => 'VITE_APPWRITE_TABLE_PRODUCTS',
'description' => 'Table ID for products table',
'value' => 'products',
'placeholder' => 'products',
'required' => false,
'type' => 'text'
],
[
'name' => 'VITE_APPWRITE_TABLE_CATEGORIES',
'description' => 'Table ID for categories table',
'value' => 'categories',
'placeholder' => 'categories',
'required' => false,
'type' => 'text'
],
[
'name' => 'VITE_APPWRITE_TABLE_CUSTOMERS',
'description' => 'Table ID for customers table',
'value' => 'customers',
'placeholder' => 'customers',
'required' => false,
'type' => 'text'
],
]
],
[
'key' => 'job-applications-formspree',
'name' => 'Job applications form with Formspree',
'tagline' => 'A simple form submission template using Formspree.',
'score' => 4, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [SiteUseCases::FORMS],
'screenshotDark' => $url . '/images/sites/templates/job-applications-formspree-dark.png',
'screenshotLight' => $url . '/images/sites/templates/job-applications-formspree-light.png',
'frameworks' => [
getFramework('REACT', [
'providerRootDirectory' => './react/formspree',
]),
],
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.7.*',
'variables' => [
[
'name' => 'VITE_FORMSPREE_FORM_ID',
'description' => 'Your Formspree form ID',
'value' => '',
'placeholder' => 'xrgkpqld',
'required' => true,
'type' => 'text'
],
]
]
];
+19 -1
View File
@@ -81,7 +81,7 @@ return [
],
[
'name' => '_APP_DOMAIN',
'description' => 'Your Appwrite domain address. When setting a public suffix domain, Appwrite will attempt to issue a valid SSL certificate automatically. When used with a dev domain, Appwrite will assign a self-signed SSL certificate. The default value is \'localhost\'.',
'description' => 'Your Appwrite domain address. When setting a public suffix domain, Appwrite will attempt to issue a valid SSL certificate automatically. When used with a dev domain, Appwrite will assign a self-signed SSL certificate. The default value is \'localhost\'. Multiple domains can be separated by commas.',
'introduction' => '',
'default' => 'localhost',
'required' => true,
@@ -357,6 +357,15 @@ return [
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_TRUSTED_HEADERS',
'description' => 'This option allows you to set the list of trusted headers, the value is a commaseparated list of HTTP header names, evaluated left-to-right for the first valid IP. Header names are treated case-insensitively.',
'introduction' => '1.8.0',
'default' => 'x-forwarded-for',
'required' => false,
'question' => '',
'filter' => ''
]
],
],
@@ -1276,6 +1285,15 @@ return [
'category' => 'GraphQL',
'description' => '',
'variables' => [
[
'name' => '_APP_GRAPHQL_INTROSPECTION',
'description' => 'Enable or disable GraphQL introspection. Set to \'enabled\' to allow schema introspection, or \'disabled\' to block it. The default value is \'enabled\'.',
'introduction' => '',
'default' => 'enabled',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_GRAPHQL_MAX_BATCH_SIZE',
'description' => 'Maximum number of batched queries per request. The default value is 10.',
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-152
View File
@@ -1,152 +0,0 @@
<?php
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Document;
use Utopia\Domains\Domain;
use Utopia\System\System;
use Utopia\Validator\IP;
use Utopia\Validator\Text;
App::init()
->groups(['console'])
->inject('project')
->action(function (Document $project) {
if ($project->getId() !== 'console') {
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN);
}
});
App::get('/v1/console/variables')
->desc('Get variables')
->groups(['api', 'projects'])
->label('scope', 'projects.read')
->label('sdk', new Method(
namespace: 'console',
group: 'console',
name: 'variables',
description: '/docs/references/console/variables.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_CONSOLE_VARIABLES,
)
],
contentType: ContentType::JSON
))
->inject('response')
->action(function (Response $response) {
$validator = new Domain(System::getEnv('_APP_DOMAIN'));
$isDomainValid = !empty(System::getEnv('_APP_DOMAIN', '')) && $validator->isKnown() && !$validator->isTest();
$validator = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME'));
$isCNAMEValid = !empty(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')) && $validator->isKnown() && !$validator->isTest();
$validator = new IP(IP::V4);
$isAValid = !empty(System::getEnv('_APP_DOMAIN_TARGET_A', '')) && ($validator->isValid(System::getEnv('_APP_DOMAIN_TARGET_A')));
$validator = new IP(IP::V6);
$isAAAAValid = !empty(System::getEnv('_APP_DOMAIN_TARGET_AAAA', '')) && $validator->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA'));
$isDomainEnabled = $isDomainValid && (
$isAAAAValid || $isAValid || $isCNAMEValid
);
$isVcsEnabled = !empty(System::getEnv('_APP_VCS_GITHUB_APP_NAME', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_APP_ID', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', ''));
$isAssistantEnabled = !empty(System::getEnv('_APP_ASSISTANT_OPENAI_API_KEY', ''));
$variables = new Document([
'_APP_DOMAIN_TARGET_CNAME' => System::getEnv('_APP_DOMAIN_TARGET_CNAME'),
'_APP_DOMAIN_TARGET_AAAA' => System::getEnv('_APP_DOMAIN_TARGET_AAAA'),
'_APP_DOMAIN_TARGET_A' => System::getEnv('_APP_DOMAIN_TARGET_A'),
// Combine CAA domain with most common flags and tag (no parameters)
'_APP_DOMAIN_TARGET_CAA' => '0 issue "' . System::getEnv('_APP_DOMAIN_TARGET_CAA') . '"',
'_APP_STORAGE_LIMIT' => +System::getEnv('_APP_STORAGE_LIMIT'),
'_APP_COMPUTE_BUILD_TIMEOUT' => +System::getEnv('_APP_COMPUTE_BUILD_TIMEOUT'),
'_APP_COMPUTE_SIZE_LIMIT' => +System::getEnv('_APP_COMPUTE_SIZE_LIMIT'),
'_APP_USAGE_STATS' => System::getEnv('_APP_USAGE_STATS'),
'_APP_VCS_ENABLED' => $isVcsEnabled,
'_APP_DOMAIN_ENABLED' => $isDomainEnabled,
'_APP_ASSISTANT_ENABLED' => $isAssistantEnabled,
'_APP_DOMAIN_SITES' => System::getEnv('_APP_DOMAIN_SITES'),
'_APP_DOMAIN_FUNCTIONS' => System::getEnv('_APP_DOMAIN_FUNCTIONS'),
'_APP_OPTIONS_FORCE_HTTPS' => System::getEnv('_APP_OPTIONS_FORCE_HTTPS'),
'_APP_DOMAINS_NAMESERVERS' => System::getEnv('_APP_DOMAINS_NAMESERVERS'),
]);
$response->dynamic($variables, Response::MODEL_CONSOLE_VARIABLES);
});
App::post('/v1/console/assistant')
->desc('Create assistant query')
->groups(['api', 'assistant'])
->label('scope', 'assistant.read')
->label('sdk', new Method(
namespace: 'assistant',
group: 'console',
name: 'chat',
description: '/docs/references/assistant/chat.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::TEXT
))
->label('abuse-limit', 15)
->label('abuse-key', 'userId:{userId}')
->param('prompt', '', new Text(2000), 'Prompt. A string containing questions asked to the AI assistant.')
->inject('response')
->action(function (string $prompt, Response $response) {
$ch = curl_init('http://appwrite-assistant:3003/v1/models/assistant/prompt');
$responseHeaders = [];
$query = json_encode(['prompt' => $prompt]);
$headers = ['accept: text/event-stream'];
$handleEvent = function ($ch, $data) use ($response) {
$response->chunk($data);
return \strlen($data);
};
curl_setopt($ch, CURLOPT_WRITEFUNCTION, $handleEvent);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0);
curl_setopt($ch, CURLOPT_TIMEOUT, 9000);
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) {
$len = strlen($header);
$header = explode(':', $header, 2);
if (count($header) < 2) { // ignore invalid headers
return $len;
}
$responseHeaders[strtolower(trim($header[0]))] = trim($header[1]);
return $len;
});
curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
curl_exec($ch);
curl_close($ch);
$response->chunk('', true);
});
+10 -6
View File
@@ -28,11 +28,12 @@ use Utopia\Validator\Text;
App::init()
->groups(['graphql'])
->inject('project')
->action(function (Document $project) {
->inject('authorization')
->action(function (Document $project, Authorization $authorization) {
if (
array_key_exists('graphql', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['graphql']
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
@@ -46,7 +47,7 @@ App::get('/v1/graphql')
namespace: 'graphql',
group: 'graphql',
name: 'get',
auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::KEY, AuthType::SESSION, AuthType::JWT],
hide: true,
description: '/docs/references/graphql/get.md',
responses: [
@@ -93,7 +94,7 @@ App::post('/v1/graphql/mutation')
namespace: 'graphql',
group: 'graphql',
name: 'mutation',
auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::KEY, AuthType::SESSION, AuthType::JWT],
description: '/docs/references/graphql/post.md',
responses: [
new SDKResponse(
@@ -144,7 +145,7 @@ App::post('/v1/graphql')
namespace: 'graphql',
group: 'graphql',
name: 'query',
auth: [AuthType::KEY, AuthType::SESSION, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::KEY, AuthType::SESSION, AuthType::JWT],
description: '/docs/references/graphql/post.md',
responses: [
new SDKResponse(
@@ -223,8 +224,11 @@ function execute(
$flags = DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE;
$validations = GraphQL::getStandardValidationRules();
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') {
if (System::getEnv('_APP_GRAPHQL_INTROSPECTION', 'enabled') === 'disabled') {
$validations[] = new DisableIntrospection();
}
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') {
$validations[] = new QueryComplexity($maxComplexity);
$validations[] = new QueryDepth($maxDepth);
}
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -20,7 +20,7 @@ App::get('/v1/locale')
group: null,
name: 'get',
description: '/docs/references/locale/get-locale.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -79,7 +79,7 @@ App::get('/v1/locale/codes')
group: null,
name: 'listCodes',
description: '/docs/references/locale/list-locale-codes.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -105,7 +105,7 @@ App::get('/v1/locale/countries')
group: null,
name: 'listCountries',
description: '/docs/references/locale/list-countries.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -142,7 +142,7 @@ App::get('/v1/locale/countries/eu')
group: null,
name: 'listCountriesEU',
description: '/docs/references/locale/list-countries-eu.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -181,7 +181,7 @@ App::get('/v1/locale/countries/phones')
group: null,
name: 'listCountriesPhones',
description: '/docs/references/locale/list-countries-phones.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -219,7 +219,7 @@ App::get('/v1/locale/continents')
group: null,
name: 'listContinents',
description: '/docs/references/locale/list-continents.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -255,7 +255,7 @@ App::get('/v1/locale/currencies')
group: null,
name: 'listCurrencies',
description: '/docs/references/locale/list-currencies.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -282,7 +282,7 @@ App::get('/v1/locale/languages')
group: null,
name: 'listLanguages',
description: '/docs/references/locale/list-languages.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
+82 -75
View File
@@ -36,6 +36,7 @@ use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Authorization\Input;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Cursor;
@@ -339,6 +340,7 @@ App::post('/v1/messaging/providers/smtp')
since: '1.8.0',
replaceWith: 'messaging.createSMTPProvider',
),
public: false,
),
new Method(
namespace: 'messaging',
@@ -872,6 +874,7 @@ App::post('/v1/messaging/providers/fcm')
since: '1.8.0',
replaceWith: 'messaging.createFCMProvider',
),
public: false,
),
new Method(
namespace: 'messaging',
@@ -961,6 +964,7 @@ App::post('/v1/messaging/providers/apns')
since: '1.8.0',
replaceWith: 'messaging.createAPNSProvider',
),
public: false,
),
new Method(
namespace: 'messaging',
@@ -1070,8 +1074,9 @@ App::get('/v1/messaging/providers')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('dbForProject')
->inject('authorization')
->inject('response')
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Response $response) {
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Authorization $authorization, Response $response) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@@ -1097,7 +1102,7 @@ App::get('/v1/messaging/providers')
}
$providerId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId));
$cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('providers', $providerId));
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Provider '{$providerId}' for the 'cursor' value not found.");
@@ -1142,7 +1147,8 @@ App::get('/v1/messaging/providers/:providerId/logs')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $providerId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->inject('audit')
->action(function (string $providerId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audit) {
$provider = $dbForProject->getDocument('providers', $providerId);
if ($provider->isEmpty()) {
@@ -1155,15 +1161,12 @@ App::get('/v1/messaging/providers/:providerId/logs')
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'))),
]);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? 25;
$offset = $grouped['offset'] ?? 0;
$audit = new Audit($dbForProject);
$resource = 'provider/' . $providerId;
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, offset: $offset, limit: $limit);
$output = [];
foreach ($logs as $i => &$log) {
@@ -1210,7 +1213,7 @@ App::get('/v1/messaging/providers/:providerId/logs')
}
$response->dynamic(new Document([
'total' => $includeTotal ? $audit->countLogsByResource($resource, $queries) : 0,
'total' => $includeTotal ? $audit->countLogsByResource($resource) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -1580,6 +1583,7 @@ App::patch('/v1/messaging/providers/smtp/:providerId')
since: '1.8.0',
replaceWith: 'messaging.updateSMTPProvider',
),
public: false,
),
new Method(
namespace: 'messaging',
@@ -2171,6 +2175,7 @@ App::patch('/v1/messaging/providers/fcm/:providerId')
since: '1.8.0',
replaceWith: 'messaging.updateFCMProvider',
),
public: false,
),
new Method(
namespace: 'messaging',
@@ -2266,6 +2271,7 @@ App::patch('/v1/messaging/providers/apns/:providerId')
since: '1.8.0',
replaceWith: 'messaging.updateAPNSProvider',
),
public: false,
),
new Method(
namespace: 'messaging',
@@ -2477,8 +2483,9 @@ App::get('/v1/messaging/topics')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('dbForProject')
->inject('authorization')
->inject('response')
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Response $response) {
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Authorization $authorization, Response $response) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@@ -2504,7 +2511,7 @@ App::get('/v1/messaging/topics')
}
$topicId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
$cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Topic '{$topicId}' for the 'cursor' value not found.");
@@ -2549,7 +2556,8 @@ App::get('/v1/messaging/topics/:topicId/logs')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $topicId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->inject('audit')
->action(function (string $topicId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audit) {
$topic = $dbForProject->getDocument('topics', $topicId);
if ($topic->isEmpty()) {
@@ -2562,15 +2570,12 @@ App::get('/v1/messaging/topics/:topicId/logs')
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'))),
]);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? 25;
$offset = $grouped['offset'] ?? 0;
$audit = new Audit($dbForProject);
$resource = 'topic/' . $topicId;
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, offset: $offset, limit: $limit);
$output = [];
@@ -2618,7 +2623,7 @@ App::get('/v1/messaging/topics/:topicId/logs')
}
$response->dynamic(new Document([
'total' => $includeTotal ? $audit->countLogsByResource($resource, $queries) : 0,
'total' => $includeTotal ? $audit->countLogsByResource($resource) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -2780,29 +2785,27 @@ App::post('/v1/messaging/topics/:topicId/subscribers')
->param('targetId', '', new UID(), 'Target ID. The target ID to link to the specified Topic ID.')
->inject('queueForEvents')
->inject('dbForProject')
->inject('authorization')
->inject('response')
->action(function (string $subscriberId, string $topicId, string $targetId, Event $queueForEvents, Database $dbForProject, Response $response) {
->action(function (string $subscriberId, string $topicId, string $targetId, Event $queueForEvents, Database $dbForProject, Authorization $authorization, Response $response) {
$subscriberId = $subscriberId == 'unique()' ? ID::unique() : $subscriberId;
$topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
$topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
if ($topic->isEmpty()) {
throw new Exception(Exception::TOPIC_NOT_FOUND);
}
$validator = new Authorization('subscribe');
if (!$validator->isValid($topic->getAttribute('subscribe'))) {
throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription());
if (!$authorization->isValid(new Input('subscribe', $topic->getAttribute('subscribe')))) {
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
}
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId));
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId));
if ($target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
$user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
$user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
$subscriber = new Document([
'$id' => $subscriberId,
@@ -2835,7 +2838,7 @@ App::post('/v1/messaging/topics/:topicId/subscribers')
default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE),
};
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute(
$authorization->skip(fn () => $dbForProject->increaseDocumentAttribute(
'topics',
$topicId,
$totalAttribute,
@@ -2880,8 +2883,9 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('dbForProject')
->inject('authorization')
->inject('response')
->action(function (string $topicId, array $queries, string $search, bool $includeTotal, Database $dbForProject, Response $response) {
->action(function (string $topicId, array $queries, string $search, bool $includeTotal, Database $dbForProject, Authorization $authorization, Response $response) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@@ -2892,7 +2896,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
$queries[] = Query::search('search', $search);
}
$topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
$topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
if ($topic->isEmpty()) {
throw new Exception(Exception::TOPIC_NOT_FOUND);
@@ -2915,7 +2919,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
}
$subscriberId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('subscribers', $subscriberId));
$cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('subscribers', $subscriberId));
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Subscriber '{$subscriberId}' for the 'cursor' value not found.");
@@ -2929,10 +2933,10 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
$subscribers = batch(\array_map(function (Document $subscriber) use ($dbForProject) {
return function () use ($subscriber, $dbForProject) {
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')));
$user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
$subscribers = batch(\array_map(function (Document $subscriber) use ($dbForProject, $authorization) {
return function () use ($subscriber, $dbForProject, $authorization) {
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')));
$user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
return $subscriber
->setAttribute('target', $target)
@@ -2972,7 +2976,8 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $subscriberId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->inject('audit')
->action(function (string $subscriberId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audit) {
$subscriber = $dbForProject->getDocument('subscribers', $subscriberId);
if ($subscriber->isEmpty()) {
@@ -2985,15 +2990,12 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs')
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'))),
]);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? 25;
$offset = $grouped['offset'] ?? 0;
$audit = new Audit($dbForProject);
$resource = 'subscriber/' . $subscriberId;
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, limit: $limit, offset: $offset);
$output = [];
@@ -3041,7 +3043,7 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs')
}
$response->dynamic(new Document([
'total' => $includeTotal ? $audit->countLogsByResource($resource, $queries) : 0,
'total' => $includeTotal ? $audit->countLogsByResource($resource) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -3067,9 +3069,10 @@ App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
->param('topicId', '', new UID(), 'Topic ID. The topic ID subscribed to.')
->param('subscriberId', '', new UID(), 'Subscriber ID.')
->inject('dbForProject')
->inject('authorization')
->inject('response')
->action(function (string $topicId, string $subscriberId, Database $dbForProject, Response $response) {
$topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
->action(function (string $topicId, string $subscriberId, Database $dbForProject, Authorization $authorization, Response $response) {
$topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
if ($topic->isEmpty()) {
throw new Exception(Exception::TOPIC_NOT_FOUND);
@@ -3081,8 +3084,8 @@ App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
throw new Exception(Exception::SUBSCRIBER_NOT_FOUND);
}
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')));
$user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')));
$user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
$subscriber
->setAttribute('target', $target)
@@ -3118,9 +3121,10 @@ App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
->param('subscriberId', '', new UID(), 'Subscriber ID.')
->inject('queueForEvents')
->inject('dbForProject')
->inject('authorization')
->inject('response')
->action(function (string $topicId, string $subscriberId, Event $queueForEvents, Database $dbForProject, Response $response) {
$topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
->action(function (string $topicId, string $subscriberId, Event $queueForEvents, Database $dbForProject, Authorization $authorization, Response $response) {
$topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
if ($topic->isEmpty()) {
throw new Exception(Exception::TOPIC_NOT_FOUND);
@@ -3143,7 +3147,7 @@ App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE),
};
Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute(
$authorization->skip(fn () => $dbForProject->decreaseDocumentAttribute(
'topics',
$topicId,
$totalAttribute,
@@ -3343,6 +3347,7 @@ App::post('/v1/messaging/messages/sms')
since: '1.8.0',
replaceWith: 'messaging.createSMS',
),
public: false,
),
new Method(
namespace: 'messaging',
@@ -3506,7 +3511,8 @@ App::post('/v1/messaging/messages/push')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, string $title, string $body, ?array $topics, ?array $users, ?array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
->inject('platform')
->action(function (string $messageId, string $title, string $body, ?array $topics, ?array $users, ?array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response, array $platform) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -3562,8 +3568,8 @@ App::post('/v1/messaging/messages/push')
throw new Exception(Exception::STORAGE_FILE_TYPE_UNSUPPORTED);
}
$host = System::getEnv('_APP_DOMAIN', 'localhost');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$endpoint = "$protocol://{$platform['apiHostname']}/v1";
$scheduleTime = $currentScheduledAt ?? $scheduledAt;
if (!\is_null($scheduleTime)) {
@@ -3583,7 +3589,7 @@ App::post('/v1/messaging/messages/push')
$image = [
'bucketId' => $bucket->getId(),
'fileId' => $file->getId(),
'url' => "{$protocol}://{$host}/v1/storage/buckets/{$bucket->getId()}/files/{$file->getId()}/push?project={$project->getId()}&jwt={$jwt}",
'url' => "{$endpoint}/storage/buckets/{$bucket->getId()}/files/{$file->getId()}/push?project={$project->getId()}&jwt={$jwt}",
];
}
@@ -3700,8 +3706,9 @@ App::get('/v1/messaging/messages')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('dbForProject')
->inject('authorization')
->inject('response')
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Response $response) {
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Authorization $authorization, Response $response) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@@ -3727,7 +3734,7 @@ App::get('/v1/messaging/messages')
}
$messageId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('messages', $messageId));
$cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('messages', $messageId));
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Message '{$messageId}' for the 'cursor' value not found.");
@@ -3772,7 +3779,8 @@ App::get('/v1/messaging/messages/:messageId/logs')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $messageId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->inject('audit')
->action(function (string $messageId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audit) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -3785,15 +3793,12 @@ App::get('/v1/messaging/messages/:messageId/logs')
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'))),
]);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? 25;
$offset = $grouped['offset'] ?? 0;
$audit = new Audit($dbForProject);
$resource = 'message/' . $messageId;
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, limit: $limit, offset: $offset);
$output = [];
@@ -3841,7 +3846,7 @@ App::get('/v1/messaging/messages/:messageId/logs')
}
$response->dynamic(new Document([
'total' => $includeTotal ? $audit->countLogsByResource($resource, $queries) : 0,
'total' => $includeTotal ? $audit->countLogsByResource($resource) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -4192,6 +4197,7 @@ App::patch('/v1/messaging/messages/sms/:messageId')
since: '1.8.0',
replaceWith: 'messaging.updateSMS',
),
public: false,
),
new Method(
namespace: 'messaging',
@@ -4394,7 +4400,8 @@ App::patch('/v1/messaging/messages/push/:messageId')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, ?bool $contentAvailable, ?bool $critical, ?string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
->inject('platform')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, ?bool $contentAvailable, ?bool $critical, ?string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response, array $platform) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -4562,9 +4569,6 @@ App::patch('/v1/messaging/messages/push/:messageId')
throw new Exception(Exception::STORAGE_FILE_TYPE_UNSUPPORTED);
}
$host = System::getEnv('_APP_DOMAIN', 'localhost');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$scheduleTime = $currentScheduledAt ?? $scheduledAt;
if (!\is_null($scheduleTime)) {
$expiry = (new \DateTime($scheduleTime))->add(new \DateInterval('P15D'))->format('U');
@@ -4580,10 +4584,13 @@ App::patch('/v1/messaging/messages/push/:messageId')
'projectId' => $project->getId(),
]);
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$endpoint = "$protocol://{$platform['apiHostname']}/v1";
$pushData['image'] = [
'bucketId' => $bucket->getId(),
'fileId' => $file->getId(),
'url' => "{$protocol}://{$host}/v1/storage/buckets/{$bucket->getId()}/files/{$file->getId()}/push?project={$project->getId()}&jwt={$jwt}"
'url' => "{$endpoint}/storage/buckets/{$bucket->getId()}/files/{$file->getId()}/push?project={$project->getId()}&jwt={$jwt}",
];
}
+30 -10
View File
@@ -69,10 +69,11 @@ App::post('/v1/migrations/appwrite')
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('platform')
->inject('user')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (array $resources, string $endpoint, string $projectId, string $apiKey, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
->action(function (array $resources, string $endpoint, string $projectId, string $apiKey, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
@@ -96,6 +97,7 @@ App::post('/v1/migrations/appwrite')
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->setUser($user)
->trigger();
@@ -128,10 +130,11 @@ App::post('/v1/migrations/firebase')
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('platform')
->inject('user')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (array $resources, string $serviceAccount, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
->action(function (array $resources, string $serviceAccount, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
$serviceAccountData = json_decode($serviceAccount, true);
if (empty($serviceAccountData)) {
@@ -163,6 +166,7 @@ App::post('/v1/migrations/firebase')
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->setUser($user)
->trigger();
@@ -200,10 +204,11 @@ App::post('/v1/migrations/supabase')
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('platform')
->inject('user')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
@@ -230,6 +235,7 @@ App::post('/v1/migrations/supabase')
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->setUser($user)
->trigger();
@@ -268,10 +274,11 @@ App::post('/v1/migrations/nhost')
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('platform')
->inject('user')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
@@ -299,6 +306,7 @@ App::post('/v1/migrations/nhost')
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->setUser($user)
->trigger();
@@ -334,7 +342,9 @@ App::post('/v1/migrations/csv/imports')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->inject('platform')
->inject('deviceForFiles')
->inject('deviceForMigrations')
->inject('queueForEvents')
@@ -347,13 +357,15 @@ App::post('/v1/migrations/csv/imports')
Response $response,
Database $dbForProject,
Database $dbForPlatform,
Authorization $authorization,
Document $project,
array $platform,
Device $deviceForFiles,
Device $deviceForMigrations,
Event $queueForEvents,
Migration $queueForMigrations
) {
$bucket = Authorization::skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
$bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
if ($internalFile) {
return $dbForPlatform->getDocument('buckets', 'default');
}
@@ -364,7 +376,7 @@ App::post('/v1/migrations/csv/imports')
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$file = Authorization::skip(fn () => $internalFile ? $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $fileId) : $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
$file = $authorization->skip(fn () => $internalFile ? $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $fileId) : $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
@@ -441,6 +453,7 @@ App::post('/v1/migrations/csv/imports')
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setProject($project)
->trigger();
$response
@@ -480,7 +493,9 @@ App::post('/v1/migrations/csv/exports')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->inject('platform')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (
@@ -497,7 +512,9 @@ App::post('/v1/migrations/csv/exports')
Response $response,
Database $dbForProject,
Database $dbForPlatform,
Authorization $authorization,
Document $project,
array $platform,
Event $queueForEvents,
Migration $queueForMigrations
) {
@@ -507,7 +524,7 @@ App::post('/v1/migrations/csv/exports')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'default'));
$bucket = $authorization->skip(fn () => $dbForPlatform->getDocument('buckets', 'default'));
if ($bucket->isEmpty()) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
@@ -520,12 +537,12 @@ App::post('/v1/migrations/csv/exports')
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
$collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty()) {
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
@@ -571,6 +588,7 @@ App::post('/v1/migrations/csv/exports')
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->trigger();
$response
@@ -903,9 +921,10 @@ App::patch('/v1/migrations/:migrationId')
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('platform')
->inject('user')
->inject('queueForMigrations')
->action(function (string $migrationId, Response $response, Database $dbForProject, Document $project, Document $user, Migration $queueForMigrations) {
->action(function (string $migrationId, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Migration $queueForMigrations) {
$migration = $dbForProject->getDocument('migrations', $migrationId);
if ($migration->isEmpty()) {
@@ -924,6 +943,7 @@ App::patch('/v1/migrations/:migrationId')
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->setUser($user)
->trigger();
+5 -4
View File
@@ -45,9 +45,10 @@ App::get('/v1/project/usage')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('authorization')
->inject('getLogsDB')
->inject('smsRates')
->action(function (string $startDate, string $endDate, string $period, Response $response, Document $project, Database $dbForProject, callable $getLogsDB, array $smsRates) {
->action(function (string $startDate, string $endDate, string $period, Response $response, Document $project, Database $dbForProject, Authorization $authorization, callable $getLogsDB, array $smsRates) {
$stats = $total = $usage = [];
$format = 'Y-m-d 00:00:00';
$firstDay = (new DateTime($startDate))->format($format);
@@ -102,7 +103,7 @@ App::get('/v1/project/usage')
'1d' => 'Y-m-d\T00:00:00.000P',
};
Authorization::skip(function () use ($dbForProject, $dbForLogs, $firstDay, $lastDay, $period, $metrics, $limit, &$total, &$stats) {
$authorization->skip(function () use ($dbForProject, $dbForLogs, $firstDay, $lastDay, $period, $metrics, $limit, &$total, &$stats) {
foreach ($metrics['total'] as $metric) {
$db = ($metric === METRIC_FILES_IMAGES_TRANSFORMED) ? $dbForLogs : $dbForProject;
@@ -286,7 +287,7 @@ App::get('/v1/project/usage')
}, $dbForProject->find('functions'));
// This total is includes free and paid SMS usage
$authPhoneTotal = Authorization::skip(fn () => $dbForProject->sum('stats', 'value', [
$authPhoneTotal = $authorization->skip(fn () => $dbForProject->sum('stats', 'value', [
Query::equal('metric', [METRIC_AUTH_METHOD_PHONE]),
Query::equal('period', ['1d']),
Query::greaterThanEqual('time', $firstDay),
@@ -294,7 +295,7 @@ App::get('/v1/project/usage')
]));
// This estimate is only for paid SMS usage
$authPhoneMetrics = Authorization::skip(fn () => $dbForProject->find('stats', [
$authPhoneMetrics = $authorization->skip(fn () => $dbForProject->find('stats', [
Query::startsWith('metric', METRIC_AUTH_METHOD_PHONE . '.'),
Query::equal('period', ['1d']),
Query::greaterThanEqual('time', $firstDay),
+33 -15
View File
@@ -21,6 +21,7 @@ use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use PHPMailer\PHPMailer\PHPMailer;
use Utopia\App;
use Utopia\Audit\Adapter\Database as AdapterDatabase;
use Utopia\Audit\Audit;
use Utopia\Cache\Cache;
use Utopia\Config\Config;
@@ -203,6 +204,7 @@ App::post('/v1/projects')
'accessedAt' => DateTime::now(),
'search' => implode(' ', [$projectId, $name]),
'database' => $dsn,
'labels' => [],
]));
} catch (Duplicate) {
throw new Exception(Exception::PROJECT_ALREADY_EXISTS);
@@ -225,6 +227,7 @@ App::post('/v1/projects')
if (!$sharedTablesV2) {
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$dbForProject = new Database($adapter, $cache);
$dbForProject->setDatabase(APP_DATABASE);
if ($sharedTables) {
$dbForProject
@@ -247,13 +250,15 @@ App::post('/v1/projects')
}
if ($create || $projectTables) {
$audit = new Audit($dbForProject);
$adapter = new AdapterDatabase($dbForProject);
$audit = new Audit($adapter);
$audit->setup();
}
if (!$create && $sharedTablesV1) {
$attributes = \array_map(fn ($attribute) => new Document($attribute), Audit::ATTRIBUTES);
$indexes = \array_map(fn (array $index) => new Document($index), Audit::INDEXES);
$adapter = new AdapterDatabase($dbForProject);
$attributes = $adapter->getAttributeDocuments();
$indexes = $adapter->getIndexDocuments();
$dbForProject->createDocument(Database::METADATA, new Document([
'$id' => ID::custom('audit'),
'$permissions' => [Permission::create(Role::any())],
@@ -294,7 +299,7 @@ App::post('/v1/projects')
// Hook allowing instant project mirroring during migration
// Outside of migration, hook is not registered and has no effect
$hooks->trigger('afterProjectCreation', [ $project, $pools, $cache ]);
$hooks->trigger('afterProjectCreation', [$project, $pools, $cache]);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -563,6 +568,7 @@ App::patch('/v1/projects/:projectId/api')
since: '1.8.0',
replaceWith: 'projects.updateAPIStatus',
),
public: false,
),
new Method(
namespace: 'projects',
@@ -620,6 +626,7 @@ App::patch('/v1/projects/:projectId/api/all')
since: '1.8.0',
replaceWith: 'projects.updateAPIStatusAll',
),
public: false,
),
new Method(
namespace: 'projects',
@@ -1476,7 +1483,7 @@ App::post('/v1/projects/:projectId/keys')
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForPlatform')
@@ -1495,8 +1502,9 @@ App::post('/v1/projects/:projectId/keys')
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'resourceInternalId' => $project->getSequence(),
'resourceId' => $project->getId(),
'resourceType' => 'projects',
'name' => $name,
'scopes' => $scopes,
'expire' => $expire,
@@ -1544,7 +1552,8 @@ App::get('/v1/projects/:projectId/keys')
}
$keys = $dbForPlatform->find('keys', [
Query::equal('projectInternalId', [$project->getSequence()]),
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
Query::limit(5000),
]);
@@ -1585,7 +1594,8 @@ App::get('/v1/projects/:projectId/keys/:keyId')
$key = $dbForPlatform->findOne('keys', [
Query::equal('$id', [$keyId]),
Query::equal('projectInternalId', [$project->getSequence()]),
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
]);
if ($key->isEmpty()) {
@@ -1615,7 +1625,7 @@ App::put('/v1/projects/:projectId/keys/:keyId')
->param('projectId', '', new UID(), 'Project unique ID.')
->param('keyId', '', new UID(), 'Key unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForPlatform')
@@ -1629,7 +1639,8 @@ App::put('/v1/projects/:projectId/keys/:keyId')
$key = $dbForPlatform->findOne('keys', [
Query::equal('$id', [$keyId]),
Query::equal('projectInternalId', [$project->getSequence()]),
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
]);
if ($key->isEmpty()) {
@@ -1680,7 +1691,8 @@ App::delete('/v1/projects/:projectId/keys/:keyId')
$key = $dbForPlatform->findOne('keys', [
Query::equal('$id', [$keyId]),
Query::equal('projectInternalId', [$project->getSequence()]),
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
]);
if ($key->isEmpty()) {
@@ -1714,7 +1726,7 @@ App::post('/v1/projects/:projectId/jwts')
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for JWT key. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for JWT key. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('duration', 900, new Range(0, 3600), 'Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds.', true)
->inject('response')
->inject('dbForPlatform')
@@ -2024,6 +2036,7 @@ App::patch('/v1/projects/:projectId/smtp')
since: '1.8.0',
replaceWith: 'projects.updateSMTP',
),
public: false,
),
new Method(
namespace: 'projects',
@@ -2076,6 +2089,7 @@ App::patch('/v1/projects/:projectId/smtp')
if ($enabled) {
$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->SMTPAuth = (!empty($username) && !empty($password));
$mail->Username = $username;
$mail->Password = $password;
$mail->Host = $host;
@@ -2091,7 +2105,7 @@ App::patch('/v1/projects/:projectId/smtp')
throw new Exception('Connection is not valid.');
}
} catch (Throwable $error) {
throw new Exception(Exception::PROJECT_SMTP_CONFIG_INVALID, 'Could not connect to SMTP server: ' . $error->getMessage());
throw new Exception(Exception::PROJECT_SMTP_CONFIG_INVALID, $error->getMessage());
}
}
@@ -2140,6 +2154,7 @@ App::post('/v1/projects/:projectId/smtp/tests')
since: '1.8.0',
replaceWith: 'projects.createSMTPTest',
),
public: false,
),
new Method(
namespace: 'projects',
@@ -2234,6 +2249,7 @@ App::get('/v1/projects/:projectId/templates/sms/:type/:locale')
since: '1.8.0',
replaceWith: 'projects.getSMSTemplate',
),
public: false,
),
new Method(
namespace: 'projects',
@@ -2400,6 +2416,7 @@ App::patch('/v1/projects/:projectId/templates/sms/:type/:locale')
since: '1.8.0',
replaceWith: 'projects.updateSMSTemplate',
),
public: false,
),
new Method(
namespace: 'projects',
@@ -2524,6 +2541,7 @@ App::delete('/v1/projects/:projectId/templates/sms/:type/:locale')
since: '1.8.0',
replaceWith: 'projects.deleteSMSTemplate',
),
public: false,
),
new Method(
namespace: 'projects',
@@ -2658,7 +2676,7 @@ App::patch('/v1/projects/:projectId/auth/session-invalidation')
$auths = $project->getAttribute('auths', []);
$auths['invalidateSessions'] = $enabled;
$dbForPlatform->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
->setAttribute('auths', $auths));
$response->dynamic($project, Response::MODEL_PROJECT);
});
File diff suppressed because it is too large Load Diff
+79 -72
View File
@@ -23,9 +23,9 @@ use Appwrite\Utopia\Database\Validator\Queries\Memberships;
use Appwrite\Utopia\Database\Validator\Queries\Teams;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumberUtil;
use MaxMind\Db\Reader;
use Utopia\Abuse\Abuse;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Auth\Proofs\Password;
@@ -58,7 +58,6 @@ use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
App::post('/v1/teams')
@@ -73,7 +72,7 @@ App::post('/v1/teams')
group: 'teams',
name: 'create',
description: '/docs/references/teams/create-team.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -87,22 +86,24 @@ App::post('/v1/teams')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('authorization')
->inject('queueForEvents')
->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Authorization $authorization, Event $queueForEvents) {
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$isAppUser = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
$isAppUser = User::isApp($authorization->getRoles());
$teamId = $teamId == 'unique()' ? ID::unique() : $teamId;
try {
$team = Authorization::skip(fn () => $dbForProject->createDocument('teams', new Document([
$team = $authorization->skip(fn () => $dbForProject->createDocument('teams', new Document([
'$id' => $teamId,
'$permissions' => [
Permission::read(Role::team($teamId)),
Permission::update(Role::team($teamId, 'owner')),
Permission::delete(Role::team($teamId, 'owner')),
],
'labels' => [],
'name' => $name,
'total' => ($isPrivilegedUser || $isAppUser) ? 0 : 1,
'prefs' => new \stdClass(),
@@ -164,7 +165,7 @@ App::get('/v1/teams')
group: 'teams',
name: 'list',
description: '/docs/references/teams/list-teams.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -238,7 +239,7 @@ App::get('/v1/teams/:teamId')
group: 'teams',
name: 'get',
description: '/docs/references/teams/get-team.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -269,7 +270,7 @@ App::get('/v1/teams/:teamId/prefs')
group: 'teams',
name: 'getPrefs',
description: '/docs/references/teams/get-team-prefs.md',
auth: [AuthType::SESSION, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -311,7 +312,7 @@ App::put('/v1/teams/:teamId')
group: 'teams',
name: 'updateName',
description: '/docs/references/teams/update-team-name.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -357,7 +358,7 @@ App::put('/v1/teams/:teamId/prefs')
group: 'teams',
name: 'updatePrefs',
description: '/docs/references/teams/update-team-prefs.md',
auth: [AuthType::SESSION, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -404,7 +405,7 @@ App::delete('/v1/teams/:teamId')
group: 'teams',
name: 'delete',
description: '/docs/references/teams/delete-team.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
@@ -432,15 +433,22 @@ App::delete('/v1/teams/:teamId')
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove team from DB');
}
// Sync delete
$deletes = new Deletes();
$deletes->deleteMemberships($getProjectDB, $team, $project);
// Async delete
if ($project->getId() === 'console') {
$queueForDeletes
->setType(DELETE_TYPE_TEAM_PROJECTS)
->setDocument($team);
->setDocument($team)
->trigger();
}
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($team);
$queueForEvents
->setParam('teamId', $team->getId())
->setPayload($response->output($team, Response::MODEL_TEAM))
@@ -463,7 +471,7 @@ App::post('/v1/teams/:teamId/memberships')
group: 'memberships',
name: 'createMembership',
description: '/docs/references/teams/create-team-membership.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -486,12 +494,13 @@ App::post('/v1/teams/:teamId/memberships')
}
return new ArrayList(new Key(), APP_LIMIT_ARRAY_PARAMS_SIZE);
}, 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](https://appwrite.io/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.', false, ['project'])
->param('url', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect the user back to your app from the invitation email. This parameter is not required when an API key is supplied. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey']) // TODO add our own built-in confirm page
->param('url', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect the user back to your app from the invitation email. This parameter is not required when an API key is supplied. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator']) // TODO add our own built-in confirm page
->param('name', '', new Text(128), 'Name of the new team member. Max length: 128 chars.', true)
->inject('response')
->inject('project')
->inject('user')
->inject('dbForProject')
->inject('authorization')
->inject('locale')
->inject('queueForMails')
->inject('queueForMessaging')
@@ -501,9 +510,9 @@ App::post('/v1/teams/:teamId/memberships')
->inject('plan')
->inject('proofForPassword')
->inject('proofForToken')
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Password $proofForPassword, Token $proofForToken) {
$isAppUser = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Authorization $authorization, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Password $proofForPassword, Token $proofForToken) {
$isAppUser = User::isApp($authorization->getRoles());
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
$url = htmlentities($url);
if (empty($url)) {
@@ -620,13 +629,13 @@ App::post('/v1/teams/:teamId/memberships')
]);
try {
$invitee = Authorization::skip(fn () => $dbForProject->createDocument('users', $userDocument));
$invitee = $authorization->skip(fn () => $dbForProject->createDocument('users', $userDocument));
} catch (Duplicate $th) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
}
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');
$isOwner = $authorization->hasRole('team:' . $team->getId() . '/owner');
if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server)
throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not allowed to send invitations for this team');
@@ -662,11 +671,11 @@ App::post('/v1/teams/:teamId/memberships')
]);
$membership = ($isPrivilegedUser || $isAppUser) ?
Authorization::skip(fn () => $dbForProject->createDocument('memberships', $membership)) :
$authorization->skip(fn () => $dbForProject->createDocument('memberships', $membership)) :
$dbForProject->createDocument('memberships', $membership);
if ($isPrivilegedUser || $isAppUser) {
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
$authorization->skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
}
} elseif ($membership->getAttribute('confirm') === false) {
$membership->setAttribute('secret', $proofForToken->hash($secret));
@@ -678,7 +687,7 @@ App::post('/v1/teams/:teamId/memberships')
}
$membership = ($isPrivilegedUser || $isAppUser) ?
Authorization::skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)) :
$authorization->skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)) :
$dbForProject->updateDocument('memberships', $membership->getId(), $membership);
} else {
throw new Exception(Exception::MEMBERSHIP_ALREADY_CONFIRMED);
@@ -770,7 +779,7 @@ App::post('/v1/teams/:teamId/memberships')
->setPreview($preview)
->setRecipient($invitee->getAttribute('email'))
->setName($invitee->getAttribute('name', ''))
->setVariables($emailVariables)
->appendVariables($emailVariables)
->trigger();
} elseif (!empty($phone)) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
@@ -800,26 +809,21 @@ App::post('/v1/teams/:teamId/memberships')
->setRecipients([$phone])
->setProviderType('SMS');
if (isset($plan['authPhone'])) {
$timelimit = $timelimit('organization:{organizationId}', $plan['authPhone'], 30 * 24 * 60 * 60); // 30 days
$timelimit
->setParam('{organizationId}', $project->getAttribute('teamId'));
$helper = PhoneNumberUtil::getInstance();
try {
$countryCode = $helper->parse($phone)->getCountryCode();
$abuse = new Abuse($timelimit);
if ($abuse->check() && System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') {
$helper = PhoneNumberUtil::getInstance();
$countryCode = $helper->parse($phone)->getCountryCode();
if (!empty($countryCode)) {
$queueForStatsUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
if (!empty($countryCode)) {
$queueForStatsUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
$queueForStatsUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
} catch (NumberParseException $e) {
// Ignore invalid phone number for country code stats
}
$queueForStatsUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
}
}
@@ -849,7 +853,7 @@ App::get('/v1/teams/:teamId/memberships')
group: 'memberships',
name: 'listMemberships',
description: '/docs/references/teams/list-team-members.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -864,7 +868,8 @@ App::get('/v1/teams/:teamId/memberships')
->inject('response')
->inject('project')
->inject('dbForProject')
->action(function (string $teamId, array $queries, string $search, bool $includeTotal, Response $response, Document $project, Database $dbForProject) {
->inject('authorization')
->action(function (string $teamId, array $queries, string $search, bool $includeTotal, Response $response, Document $project, Database $dbForProject, Authorization $authorization) {
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
@@ -934,7 +939,7 @@ App::get('/v1/teams/:teamId/memberships')
'mfa' => $project->getAttribute('auths', [])['membershipsMfa'] ?? true,
];
$roles = Authorization::getRoles();
$roles = $authorization->getRoles();
$isPrivilegedUser = User::isPrivileged($roles);
$isAppUser = User::isApp($roles);
@@ -992,7 +997,7 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
group: 'memberships',
name: 'getMembership',
description: '/docs/references/teams/get-team-member.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -1005,7 +1010,8 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
->inject('response')
->inject('project')
->inject('dbForProject')
->action(function (string $teamId, string $membershipId, Response $response, Document $project, Database $dbForProject) {
->inject('authorization')
->action(function (string $teamId, string $membershipId, Response $response, Document $project, Database $dbForProject, Authorization $authorization) {
$team = $dbForProject->getDocument('teams', $teamId);
@@ -1025,7 +1031,7 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
'mfa' => $project->getAttribute('auths', [])['membershipsMfa'] ?? true,
];
$roles = Authorization::getRoles();
$roles = $authorization->getRoles();
$isPrivilegedUser = User::isPrivileged($roles);
$isAppUser = User::isApp($roles);
@@ -1079,7 +1085,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
group: 'memberships',
name: 'updateMembership',
description: '/docs/references/teams/update-team-membership.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -1104,8 +1110,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('authorization')
->inject('queueForEvents')
->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents) {
->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Authorization $authorization, Event $queueForEvents) {
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
@@ -1122,9 +1129,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
throw new Exception(Exception::USER_NOT_FOUND);
}
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$isAppUser = User::isApp(Authorization::getRoles());
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
$isAppUser = User::isApp($authorization->getRoles());
$isOwner = $authorization->hasRole('team:' . $team->getId() . '/owner');
if ($project->getId() === 'console') {
// Quick check: fetch up to 2 owners to determine if only one exists
@@ -1189,7 +1196,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
group: 'memberships',
name: 'updateMembershipStatus',
description: '/docs/references/teams/update-team-membership-status.md',
auth: [AuthType::SESSION, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -1205,12 +1212,13 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('authorization')
->inject('project')
->inject('geodb')
->inject('queueForEvents')
->inject('store')
->inject('proofForToken')
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) {
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) {
$protocol = $request->getProtocol();
$membership = $dbForProject->getDocument('memberships', $membershipId);
@@ -1219,7 +1227,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
throw new Exception(Exception::MEMBERSHIP_NOT_FOUND);
}
$team = Authorization::skip(fn () => $dbForProject->getDocument('teams', $teamId));
$team = $authorization->skip(fn () => $dbForProject->getDocument('teams', $teamId));
if ($team->isEmpty()) {
throw new Exception(Exception::TEAM_NOT_FOUND);
@@ -1255,11 +1263,11 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->setAttribute('confirm', true)
;
Authorization::skip(fn () => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true)));
$authorization->skip(fn () => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true)));
// Create session for the user if not logged in
if (!$hasSession) {
Authorization::setRole(Role::user($user->getId())->toString());
$authorization->addRole(Role::user($user->getId())->toString());
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
@@ -1287,7 +1295,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
$session = $dbForProject->createDocument('sessions', $session);
Authorization::setRole(Role::user($userId)->toString());
$authorization->addRole(Role::user($userId)->toString());
$encoded = $store
->setProperty('id', $user->getId())
@@ -1325,7 +1333,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
$dbForProject->purgeCachedDocument('users', $user->getId());
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
$authorization->skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
$queueForEvents
->setParam('userId', $user->getId())
@@ -1354,7 +1362,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
group: 'memberships',
name: 'deleteMembership',
description: '/docs/references/teams/delete-team-membership.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
@@ -1369,8 +1377,9 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
->inject('project')
->inject('response')
->inject('dbForProject')
->inject('authorization')
->inject('queueForEvents')
->action(function (string $teamId, string $membershipId, Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents) {
->action(function (string $teamId, string $membershipId, Document $user, Document $project, Response $response, Database $dbForProject, Authorization $authorization, Event $queueForEvents) {
$membership = $dbForProject->getDocument('memberships', $membershipId);
@@ -1428,7 +1437,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
$dbForProject->purgeCachedDocument('users', $profile->getId());
if ($membership->getAttribute('confirm')) { // Count only confirmed members
Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute('teams', $team->getId(), 'total', 1, 0));
$authorization->skip(fn () => $dbForProject->decreaseDocumentAttribute('teams', $team->getId(), 'total', 1, 0));
}
$queueForEvents
@@ -1465,7 +1474,8 @@ App::get('/v1/teams/:teamId/logs')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $teamId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->inject('audit')
->action(function (string $teamId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audit) {
$team = $dbForProject->getDocument('teams', $teamId);
@@ -1479,15 +1489,12 @@ App::get('/v1/teams/:teamId/logs')
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'))),
]);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? 25;
$offset = $grouped['offset'] ?? 0;
$audit = new Audit($dbForProject);
$resource = 'team/' . $team->getId();
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, offset: $offset, limit: $limit);
$output = [];
@@ -1534,7 +1541,7 @@ App::get('/v1/teams/:teamId/logs')
}
}
$response->dynamic(new Document([
'total' => $includeTotal ? $audit->countLogsByResource($resource, $queries) : 0,
'total' => $includeTotal ? $audit->countLogsByResource($resource) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
+63 -56
View File
@@ -238,7 +238,7 @@ App::post('/v1/users')
group: 'users',
name: 'create',
description: '/docs/references/users/create-user.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -275,7 +275,7 @@ App::post('/v1/users/bcrypt')
group: 'users',
name: 'createBcryptUser',
description: '/docs/references/users/create-bcrypt-user.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -313,7 +313,7 @@ App::post('/v1/users/md5')
group: 'users',
name: 'createMD5User',
description: '/docs/references/users/create-md5-user.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -350,7 +350,7 @@ App::post('/v1/users/argon2')
group: 'users',
name: 'createArgon2User',
description: '/docs/references/users/create-argon2-user.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -387,7 +387,7 @@ App::post('/v1/users/sha')
group: 'users',
name: 'createSHAUser',
description: '/docs/references/users/create-sha-user.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -428,7 +428,7 @@ App::post('/v1/users/phpass')
group: 'users',
name: 'createPHPassUser',
description: '/docs/references/users/create-phpass-user.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -465,7 +465,7 @@ App::post('/v1/users/scrypt')
group: 'users',
name: 'createScryptUser',
description: '/docs/references/users/create-scrypt-user.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -513,7 +513,7 @@ App::post('/v1/users/scrypt-modified')
group: 'users',
name: 'createScryptModifiedUser',
description: '/docs/references/users/create-scrypt-modified-user.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -650,7 +650,7 @@ App::get('/v1/users')
group: 'users',
name: 'list',
description: '/docs/references/users/list-users.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -729,7 +729,7 @@ App::get('/v1/users/:userId')
group: 'users',
name: 'get',
description: '/docs/references/users/get-user.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -760,7 +760,7 @@ App::get('/v1/users/:userId/prefs')
group: 'users',
name: 'getPrefs',
description: '/docs/references/users/get-user-prefs.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -831,7 +831,7 @@ App::get('/v1/users/:userId/sessions')
group: 'sessions',
name: 'listSessions',
description: '/docs/references/users/list-user-sessions.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -875,7 +875,7 @@ App::get('/v1/users/:userId/memberships')
group: 'memberships',
name: 'listMemberships',
description: '/docs/references/users/list-user-memberships.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -930,7 +930,7 @@ App::get('/v1/users/:userId/logs')
group: 'logs',
name: 'listLogs',
description: '/docs/references/users/list-user-logs.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -945,7 +945,8 @@ App::get('/v1/users/:userId/logs')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function (string $userId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
->inject('audit')
->action(function (string $userId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audit) {
$user = $dbForProject->getDocument('users', $userId);
@@ -957,13 +958,12 @@ App::get('/v1/users/:userId/logs')
} 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);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? 25;
$offset = $grouped['offset'] ?? 0;
$logs = $audit->getLogsByUser($user->getSequence(), limit: $limit, offset: $offset);
$output = [];
foreach ($logs as $i => &$log) {
$log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN';
@@ -1003,7 +1003,7 @@ App::get('/v1/users/:userId/logs')
}
$response->dynamic(new Document([
'total' => $includeTotal ? $audit->countLogsByUser($user->getSequence(), $queries) : 0,
'total' => $includeTotal ? $audit->countLogsByUser($user->getSequence()) : 0,
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -1082,7 +1082,7 @@ App::get('/v1/users/identities')
group: 'identities',
name: 'listIdentities',
description: '/docs/references/users/list-identities.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -1152,7 +1152,7 @@ App::patch('/v1/users/:userId/status')
group: 'users',
name: 'updateStatus',
description: '/docs/references/users/update-user-status.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -1193,7 +1193,7 @@ App::put('/v1/users/:userId/labels')
group: 'users',
name: 'updateLabels',
description: '/docs/references/users/update-user-labels.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -1236,7 +1236,7 @@ App::patch('/v1/users/:userId/verification/phone')
group: 'users',
name: 'updatePhoneVerification',
description: '/docs/references/users/update-user-phone-verification.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -1278,7 +1278,7 @@ App::patch('/v1/users/:userId/name')
group: 'users',
name: 'updateName',
description: '/docs/references/users/update-user-name.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -1321,7 +1321,7 @@ App::patch('/v1/users/:userId/password')
group: 'users',
name: 'updatePassword',
description: '/docs/references/users/update-user-password.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -1420,7 +1420,7 @@ App::patch('/v1/users/:userId/email')
group: 'users',
name: 'updateEmail',
description: '/docs/references/users/update-user-email.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -1531,7 +1531,7 @@ App::patch('/v1/users/:userId/phone')
group: 'users',
name: 'updatePhone',
description: '/docs/references/users/update-user-phone.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -1621,7 +1621,7 @@ App::patch('/v1/users/:userId/verification')
group: 'users',
name: 'updateEmailVerification',
description: '/docs/references/users/update-user-email-verification.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -1659,7 +1659,7 @@ App::patch('/v1/users/:userId/prefs')
group: 'users',
name: 'updatePrefs',
description: '/docs/references/users/update-user-prefs.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -1806,7 +1806,7 @@ App::patch('/v1/users/:userId/mfa')
group: 'users',
name: 'updateMfa',
description: '/docs/references/users/update-user-mfa.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -1817,13 +1817,14 @@ App::patch('/v1/users/:userId/mfa')
since: '1.8.0',
replaceWith: 'users.updateMFA',
),
public: false,
),
new Method(
namespace: 'users',
group: 'users',
name: 'updateMFA',
description: '/docs/references/users/update-user-mfa.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -1865,7 +1866,7 @@ App::get('/v1/users/:userId/mfa/factors')
group: 'mfa',
name: 'listMfaFactors',
description: '/docs/references/users/list-mfa-factors.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -1876,13 +1877,14 @@ App::get('/v1/users/:userId/mfa/factors')
since: '1.8.0',
replaceWith: 'users.listMFAFactors',
),
public: false,
),
new Method(
namespace: 'users',
group: 'mfa',
name: 'listMFAFactors',
description: '/docs/references/users/list-mfa-factors.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -1923,7 +1925,7 @@ App::get('/v1/users/:userId/mfa/recovery-codes')
group: 'mfa',
name: 'getMfaRecoveryCodes',
description: '/docs/references/users/get-mfa-recovery-codes.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -1934,13 +1936,14 @@ App::get('/v1/users/:userId/mfa/recovery-codes')
since: '1.8.0',
replaceWith: 'users.getMFARecoveryCodes',
),
public: false,
),
new Method(
namespace: 'users',
group: 'mfa',
name: 'getMFARecoveryCodes',
description: '/docs/references/users/get-mfa-recovery-codes.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -1987,7 +1990,7 @@ App::patch('/v1/users/:userId/mfa/recovery-codes')
group: 'mfa',
name: 'createMfaRecoveryCodes',
description: '/docs/references/users/create-mfa-recovery-codes.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -1998,13 +2001,14 @@ App::patch('/v1/users/:userId/mfa/recovery-codes')
since: '1.8.0',
replaceWith: 'users.createMFARecoveryCodes',
),
public: false,
),
new Method(
namespace: 'users',
group: 'mfa',
name: 'createMFARecoveryCodes',
description: '/docs/references/users/create-mfa-recovery-codes.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -2058,7 +2062,7 @@ App::put('/v1/users/:userId/mfa/recovery-codes')
group: 'mfa',
name: 'updateMfaRecoveryCodes',
description: '/docs/references/users/update-mfa-recovery-codes.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -2069,19 +2073,21 @@ App::put('/v1/users/:userId/mfa/recovery-codes')
since: '1.8.0',
replaceWith: 'users.updateMFARecoveryCodes',
),
public: false,
),
new Method(
namespace: 'users',
group: 'mfa',
name: 'updateMFARecoveryCodes',
description: '/docs/references/users/update-mfa-recovery-codes.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_MFA_RECOVERY_CODES,
)
]
],
public: false,
)
])
->param('userId', '', new UID(), 'User ID.')
@@ -2128,7 +2134,7 @@ App::delete('/v1/users/:userId/mfa/authenticators/:type')
group: 'mfa',
name: 'deleteMfaAuthenticator',
description: '/docs/references/users/delete-mfa-authenticator.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
@@ -2140,13 +2146,14 @@ App::delete('/v1/users/:userId/mfa/authenticators/:type')
since: '1.8.0',
replaceWith: 'users.deleteMFAAuthenticator',
),
public: false,
),
new Method(
namespace: 'users',
group: 'mfa',
name: 'deleteMFAAuthenticator',
description: '/docs/references/users/delete-mfa-authenticator.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
@@ -2195,7 +2202,7 @@ App::post('/v1/users/:userId/sessions')
group: 'sessions',
name: 'createSession',
description: '/docs/references/users/create-session.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -2287,7 +2294,7 @@ App::post('/v1/users/:userId/tokens')
group: 'sessions',
name: 'createToken',
description: '/docs/references/users/create-token.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -2352,7 +2359,7 @@ App::delete('/v1/users/:userId/sessions/:sessionId')
group: 'sessions',
name: 'deleteSession',
description: '/docs/references/users/delete-user-session.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
@@ -2403,7 +2410,7 @@ App::delete('/v1/users/:userId/sessions')
group: 'sessions',
name: 'deleteSessions',
description: '/docs/references/users/delete-user-sessions.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
@@ -2453,7 +2460,7 @@ App::delete('/v1/users/:userId')
group: 'users',
name: 'delete',
description: '/docs/references/users/delete.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
@@ -2563,7 +2570,7 @@ App::delete('/v1/users/identities/:identityId')
group: 'identities',
name: 'deleteIdentity',
description: '/docs/references/users/delete-identity.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
@@ -2603,7 +2610,7 @@ App::post('/v1/users/:userId/jwts')
group: 'sessions',
name: 'createJWT',
description: '/docs/references/users/create-user-jwt.md',
auth: [AuthType::KEY],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -2671,8 +2678,8 @@ App::get('/v1/users/usage')
->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->inject('register')
->action(function (string $range, Response $response, Database $dbForProject) {
->inject('authorization')
->action(function (string $range, Response $response, Database $dbForProject, Authorization $authorization) {
$periods = Config::getParam('usage', []);
$stats = $usage = [];
@@ -2682,7 +2689,7 @@ App::get('/v1/users/usage')
METRIC_SESSIONS,
];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
$authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $count => $metric) {
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
+56 -45
View File
@@ -4,7 +4,6 @@ use Appwrite\Auth\OAuth2\Github as OAuth2Github;
use Appwrite\Event\Build;
use Appwrite\Event\Delete;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Redirect;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
@@ -77,7 +76,7 @@ use Utopia\VCS\Exception\RepositoryNotFound;
use function Swoole\Coroutine\batch;
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForPlatform, Build $queueForBuilds, callable $getProjectDB, Request $request) {
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForPlatform, Authorization $authorization, Build $queueForBuilds, callable $getProjectDB, Request $request, array $platform) {
$errors = [];
foreach ($repositories as $repository) {
try {
@@ -88,12 +87,12 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
}
$projectId = $repository->getAttribute('projectId');
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
$dbForProject = $getProjectDB($project);
$resourceCollection = $resourceType === "function" ? 'functions' : 'sites';
$resourceId = $repository->getAttribute('resourceId');
$resource = Authorization::skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId));
$resource = $authorization->skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId));
$resourceInternalId = $resource->getSequence();
$deploymentId = ID::unique();
@@ -133,7 +132,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$commentStatus = $isAuthorized ? 'waiting' : 'failed';
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$hostname = $platform['consoleHostname'] ?? '';
$authorizeUrl = $protocol . '://' . $hostname . "/console/git/authorize-contributor?projectId={$projectId}&installationId={$installationId}&repositoryId={$repositoryId}&providerPullRequestId={$providerPullRequestId}";
@@ -142,7 +141,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$latestCommentId = '';
if (!empty($providerPullRequestId) && $resource->getAttribute('providerSilentMode', false) === false) {
$latestComment = Authorization::skip(fn () => $dbForPlatform->findOne('vcsComments', [
$latestComment = $authorization->skip(fn () => $dbForPlatform->findOne('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerPullRequestId', [$providerPullRequestId]),
Query::orderDesc('$createdAt'),
@@ -175,24 +174,24 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
if ($lockAcquired) {
// Wrap in try/finally to ensure lock file gets deleted
try {
$comment = new Comment();
$comment = new Comment($platform);
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
} finally {
Authorization::skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
} else {
$comment = new Comment();
$comment = new Comment($platform);
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
$latestCommentId = \strval($github->createComment($owner, $repositoryName, $providerPullRequestId, $comment->generateComment()));
if (!empty($latestCommentId)) {
$teamId = $project->getAttribute('teamId', '');
$latestComment = Authorization::skip(fn () => $dbForPlatform->createDocument('vcsComments', new Document([
$latestComment = $authorization->skip(fn () => $dbForPlatform->createDocument('vcsComments', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
@@ -213,7 +212,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
}
}
} elseif (!empty($providerBranch)) {
$latestComments = Authorization::skip(fn () => $dbForPlatform->find('vcsComments', [
$latestComments = $authorization->skip(fn () => $dbForPlatform->find('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerBranch', [$providerBranch]),
Query::orderDesc('$createdAt'),
@@ -246,13 +245,13 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
if ($lockAcquired) {
// Wrap in try/finally to ensure lock file gets deleted
try {
$comment = new Comment();
$comment = new Comment($platform);
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
} finally {
Authorization::skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
}
@@ -295,7 +294,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$commands[] = $resource->getAttribute('commands', '');
}
$deployment = Authorization::skip(fn () => $dbForProject->createDocument('deployments', new Document([
$deployment = $authorization->skip(fn () => $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
@@ -307,6 +306,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
'resourceType' => $resourceCollection,
'entrypoint' => $resource->getAttribute('entrypoint', ''),
'buildCommands' => \implode(' && ', $commands),
'startCommand' => $resource->getAttribute('startCommand', ''),
'buildOutput' => $resource->getAttribute('outputDirectory', ''),
'adapter' => $resource->getAttribute('adapter', ''),
'fallbackFile' => $resource->getAttribute('fallbackFile', ''),
@@ -335,17 +335,17 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
Authorization::skip(fn () => $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource));
$authorization->skip(fn () => $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource));
if ($resource->getCollection() === 'sites') {
$projectId = $project->getId();
// Deployment preview
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$sitesDomain = $platform['sitesDomain'];
$domain = ID::unique() . "." . $sitesDomain;
$ruleId = md5($domain);
$previewRuleId = $ruleId;
Authorization::skip(
$authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
@@ -378,7 +378,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$domain = "branch-{$branchPrefix}-{$resourceProjectHash}.{$sitesDomain}";
$ruleId = md5($domain);
try {
Authorization::skip(
$authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
@@ -409,7 +409,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$domain = "commit-" . substr($providerCommitHash, 0, 16) . ".{$sitesDomain}";
$ruleId = md5($domain);
try {
Authorization::skip(
$authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
@@ -461,19 +461,19 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
if ($lockAcquired) {
// Wrap in try/finally to ensure lock file gets deleted
try {
$rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId));
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId));
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$previewUrl = !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
if (!empty($previewUrl)) {
$comment = new Comment();
$comment = new Comment($platform);
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, $previewUrl);
$github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment());
}
} finally {
Authorization::skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
}
@@ -542,12 +542,12 @@ App::get('/v1/vcs/github/authorize')
type: MethodType::WEBAUTH,
hide: true,
))
->param('success', '', fn ($platforms) => new Redirect($platforms), 'URL to redirect back to console after a successful installation attempt.', true, ['platforms'])
->param('failure', '', fn ($platforms) => new Redirect($platforms), 'URL to redirect back to console after a failed installation attempt.', true, ['platforms'])
->inject('request')
->param('success', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to console after a successful installation attempt.', true, ['redirectValidator'])
->param('failure', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to console after a failed installation attempt.', true, ['redirectValidator'])
->inject('response')
->inject('project')
->action(function (string $success, string $failure, Request $request, Response $response, Document $project) {
->inject('platform')
->action(function (string $success, string $failure, Response $response, Document $project, array $platform) {
$state = \json_encode([
'projectId' => $project->getId(),
'success' => $success,
@@ -556,7 +556,7 @@ App::get('/v1/vcs/github/authorize')
$appName = System::getEnv('_APP_VCS_GITHUB_APP_NAME');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$hostname = $platform['consoleHostname'] ?? '';
if (empty($appName)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'GitHub App name is not configured. Please configure VCS (Version Control System) variables in .env file.');
@@ -585,10 +585,10 @@ App::get('/v1/vcs/github/callback')
->inject('gitHub')
->inject('user')
->inject('project')
->inject('request')
->inject('response')
->inject('dbForPlatform')
->action(function (string $providerInstallationId, string $setupAction, string $state, string $code, GitHub $github, Document $user, Document $project, Request $request, Response $response, Database $dbForPlatform) {
->inject('platform')
->action(function (string $providerInstallationId, string $setupAction, string $state, string $code, GitHub $github, Document $user, Document $project, Response $response, Database $dbForPlatform, array $platform) {
if (empty($state)) {
$error = 'Installation requests from organisation members for the Appwrite GitHub App are currently unsupported. To proceed with the installation, login to the Appwrite Console and install the GitHub App.';
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $error);
@@ -615,7 +615,7 @@ App::get('/v1/vcs/github/callback')
$region = $project->getAttribute('region', 'default');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$hostname = $platform['consoleHostname'] ?? '';
$defaultState = [
'success' => $protocol . '://' . $hostname . "/console/project-$region-$projectId/settings/git-installations",
@@ -1477,10 +1477,12 @@ App::post('/v1/vcs/github/events')
->inject('request')
->inject('response')
->inject('dbForPlatform')
->inject('authorization')
->inject('getProjectDB')
->inject('queueForBuilds')
->inject('platform')
->action(
function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, Authorization $authorization, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) {
$payload = $request->getRawPayload();
$signatureRemote = $request->getHeader('x-hub-signature-256', '');
$signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', '');
@@ -1516,14 +1518,14 @@ App::post('/v1/vcs/github/events')
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
//find resourceId from relevant resources table
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::limit(100),
]));
// create new deployment only on push (not committed by us) and not when branch is created or deleted
if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchCreated && !$providerBranchDeleted) {
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform);
}
} elseif ($event == $github::EVENT_INSTALLATION) {
if ($parsedPayload["action"] == "deleted") {
@@ -1536,16 +1538,16 @@ App::post('/v1/vcs/github/events')
]);
foreach ($installations as $installation) {
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('installationInternalId', [$installation->getSequence()]),
Query::limit(1000)
]));
foreach ($repositories as $repository) {
Authorization::skip(fn () => $dbForPlatform->deleteDocument('repositories', $repository->getId()));
$authorization->skip(fn () => $dbForPlatform->deleteDocument('repositories', $repository->getId()));
}
Authorization::skip(fn () => $dbForPlatform->deleteDocument('installations', $installation->getId()));
$authorization->skip(fn () => $dbForPlatform->deleteDocument('installations', $installation->getId()));
}
}
} elseif ($event == $github::EVENT_PULL_REQUEST) {
@@ -1574,12 +1576,12 @@ App::post('/v1/vcs/github/events')
$providerCommitAuthor = $commitDetails["commitAuthor"] ?? '';
$providerCommitMessage = $commitDetails["commitMessage"] ?? '';
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt')
]));
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform);
} elseif ($parsedPayload["action"] == "closed") {
// Allowed external contributions cleanup
@@ -1588,7 +1590,7 @@ App::post('/v1/vcs/github/events')
$external = $parsedPayload["external"] ?? true;
if ($external) {
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt')
]));
@@ -1599,7 +1601,7 @@ App::post('/v1/vcs/github/events')
if (\in_array($providerPullRequestId, $providerPullRequestIds)) {
$providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]);
$repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds);
$repository = Authorization::skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
$repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
}
}
}
@@ -1783,20 +1785,21 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
->param('repositoryId', '', new Text(256), 'VCS Repository Id')
->param('providerPullRequestId', '', new Text(256), 'GitHub Pull Request Id')
->inject('gitHub')
->inject('request')
->inject('response')
->inject('project')
->inject('dbForPlatform')
->inject('authorization')
->inject('getProjectDB')
->inject('queueForBuilds')
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
->inject('platform')
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForPlatform, Authorization $authorization, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$repository = Authorization::skip(fn () => $dbForPlatform->findOne('repositories', [
$repository = $authorization->skip(fn () => $dbForPlatform->findOne('repositories', [
Query::equal('$id', [$repositoryId]),
Query::equal('projectInternalId', [$project->getSequence()])
]));
@@ -1814,7 +1817,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
// TODO: Delete from array when PR is closed
$repository = Authorization::skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
$repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
@@ -1837,8 +1840,16 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
$providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? '';
$providerCommitHash = $pullRequestResponse['head']['sha'] ?? '';
$providerBranchUrl = $pullRequestResponse['head']['repo']['html_url'] ?? '';
$providerRepositoryName = $pullRequestResponse['head']['repo']['name'] ?? '';
$providerRepositoryUrl = $pullRequestResponse['head']['repo']['html_url'] ?? '';
$providerRepositoryOwner = $pullRequestResponse['head']['repo']['owner']['login'] ?? '';
$providerCommitAuthor = $pullRequestResponse['head']['user']['login'] ?? '';
$providerCommitAuthorUrl = $pullRequestResponse['head']['user']['html_url'] ?? '';
$providerCommitMessage = $pullRequestResponse['title'] ?? '';
$providerCommitUrl = $pullRequestResponse['html_url'] ?? '';
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, '', '', '', '', $providerCommitHash, '', '', '', '', $providerPullRequestId, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform);
$response->noContent();
});
+345 -218
View File
@@ -6,11 +6,12 @@ use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Network\Validator\Origin;
use Appwrite\Network\Cors;
use Appwrite\Platform\Appwrite;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -39,6 +40,7 @@ use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
@@ -51,24 +53,25 @@ use Utopia\Logger\Log\User;
use Utopia\Logger\Logger;
use Utopia\Platform\Service;
use Utopia\System\System;
use Utopia\Validator;
use Utopia\Validator\Text;
Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey)
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
{
$host = $request->getHostname() ?? '';
if (!empty($previewHostname)) {
$host = $previewHostname;
}
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
// TODO: (@Meldiron) Remove after 1.7.x migration
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($host)));
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', md5($host)));
} else {
$rule = Authorization::skip(
$rule = $authorization->skip(
fn () => $dbForPlatform->find('rules', [
Query::equal('domain', [$host]),
Query::limit(1)
@@ -77,39 +80,49 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
}
$errorView = __DIR__ . '/../views/general/error.phtml';
$url = (System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https') . '://' . System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$url = $protocol . '://' . $platform['consoleHostname'];
$platformHostnames = $platform['hostnames'] ?? [];
if ($rule->isEmpty()) {
$appDomainFunctionsFallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', '');
$appDomainFunctions = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
$appDomainSites = System::getEnv('_APP_DOMAIN_SITES', '');
if (!empty($appDomainFunctionsFallback) && \str_ends_with($host, $appDomainFunctionsFallback)) {
$appDomainFunctions = $appDomainFunctionsFallback;
}
if ($host === $appDomainFunctions || $host === $appDomainSites) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain cannot be used for security reasons. Please use any subdomain instead.', view: $errorView);
}
if (\str_ends_with($host, $appDomainFunctions) || \str_ends_with($host, $appDomainSites)) {
$exception = new AppwriteException(AppwriteException::RULE_NOT_FOUND, 'This domain is not connected to any Appwrite resources. Visit domains tab under function/site settings to configure it.', view: $errorView);
$exception->addCTA('Start with this domain', $url . '/console');
throw $exception;
}
if (System::getEnv('_APP_OPTIONS_ROUTER_PROTECTION', 'disabled') === 'enabled') {
if ($host !== 'localhost' && $host !== APP_HOSTNAME_INTERNAL && $host !== System::getEnv('_APP_CONSOLE_DOMAIN', '')) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Router protection does not allow accessing Appwrite over this domain. Please add it as custom domain to your project or disable _APP_OPTIONS_ROUTER_PROTECTION environment variable.', view: $errorView);
$denyDomains = [];
$denyEnvVars = [
System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''),
System::getEnv('_APP_DOMAIN_FUNCTIONS', ''),
System::getEnv('_APP_DOMAIN_SITES', ''),
];
foreach ($denyEnvVars as $denyEnvVar) {
foreach (\explode(',', $denyEnvVar) as $denyDomain) {
if (empty($denyDomain)) {
continue;
}
$denyDomains[] = $denyDomain;
}
}
foreach ($denyDomains as $denyDomain) {
if ($host === $denyDomain) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain cannot be used for security reasons. Please use any subdomain instead.', view: $errorView);
}
if (\str_ends_with($host, $denyDomain)) {
$exception = new AppwriteException(AppwriteException::RULE_NOT_FOUND, 'This domain is not connected to any Appwrite resources. Visit domains tab under function/site settings to configure it.', view: $errorView);
$exception->addCTA('Start with this domain', $url . '/console');
throw $exception;
}
}
if (!in_array($host, $platformHostnames)) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Router protection does not allow accessing Appwrite over this domain. Please add it as custom domain to your project or disable _APP_OPTIONS_ROUTER_PROTECTION environment variable.', view: $errorView);
}
// Act as API - no Proxy logic
return false;
}
$projectId = $rule->getAttribute('projectId');
$project = Authorization::skip(
$project = $authorization->skip(
fn () => $dbForPlatform->getDocument('projects', $projectId)
);
@@ -117,7 +130,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
$accessedAt = $project->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$project->setAttribute('accessedAt', DateTime::now());
Authorization::skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
}
/**
@@ -143,7 +156,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
if ($type === 'deployment') {
if (System::getEnv('_APP_OPTIONS_ROUTER_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS
if ($request->getProtocol() !== 'https' && $request->getHostname() !== APP_HOSTNAME_INTERNAL) {
if ($request->getProtocol() !== 'https') {
if ($request->getMethod() !== Request::METHOD_GET) {
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.', view: $errorView);
}
@@ -156,7 +169,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
/** @var Document $deployment */
if (!empty($rule->getAttribute('deploymentId', ''))) {
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId')));
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId')));
} else {
// 1.6.x DB schema compatibility
// TODO: Make sure deploymentId is never empty, and remove this code
@@ -170,15 +183,15 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
// Document of site or function
$resource = $resourceType === 'function' ?
Authorization::skip(fn () => $dbForProject->getDocument('functions', $resourceId)) :
Authorization::skip(fn () => $dbForProject->getDocument('sites', $resourceId));
$authorization->skip(fn () => $dbForProject->getDocument('functions', $resourceId)) :
$authorization->skip(fn () => $dbForProject->getDocument('sites', $resourceId));
// ID of active deployments
// Attempts to use attribute from both schemas (1.6 and 1.7)
$activeDeploymentId = $resource->getAttribute('deploymentId', $resource->getAttribute('deployment', ''));
// Get deployment document, as intended originally
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $activeDeploymentId));
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $activeDeploymentId));
}
if ($deployment->getAttribute('resourceType', '') === 'functions') {
@@ -197,8 +210,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
}
$resource = $type === 'function' ?
Authorization::skip(fn () => $dbForProject->getDocument('functions', $deployment->getAttribute('resourceId', ''))) :
Authorization::skip(fn () => $dbForProject->getDocument('sites', $deployment->getAttribute('resourceId', '')));
$authorization->skip(fn () => $dbForProject->getDocument('functions', $deployment->getAttribute('resourceId', ''))) :
$authorization->skip(fn () => $dbForProject->getDocument('sites', $deployment->getAttribute('resourceId', '')));
$isPreview = $type === 'function' ? false : ($rule->getAttribute('trigger', '') !== 'manual');
@@ -240,7 +253,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
$userExists = false;
$userId = $payload['userId'] ?? '';
if (!empty($userId)) {
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
$user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId));
if (!$user->isEmpty() && $user->getAttribute('status', false)) {
$userExists = true;
}
@@ -253,7 +266,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
}
$membershipExists = false;
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
if (!$project->isEmpty() && isset($user)) {
$teamId = $project->getAttribute('teamId', '');
$membership = $user->find('teamId', $teamId, 'memberships');
@@ -268,7 +281,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
}
if (!$authorized) {
$url = (System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https') . "://" . System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$url = $protocol . "://" . $platform['consoleHostname'];
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
@@ -456,9 +469,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
}
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_DOMAIN');
$endpoint = $protocol . '://' . $hostname . "/v1";
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$endpoint = "$protocol://{$platform['apiHostname']}/v1";
// Appwrite vars
if ($type === 'function') {
@@ -801,6 +813,20 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
->setProject($project)
->trigger();
/* cleanup */
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
$resourceType = $type === 'function'
? RESOURCE_TYPE_FUNCTIONS
: RESOURCE_TYPE_SITES;
$queueForDeletes
->setProject($project)
->setResourceType($resourceType)
->setResource($resource->getSequence())
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
->trigger();
}
return true;
} elseif ($type === 'api') {
return false;
@@ -811,8 +837,6 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
} else {
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown resource type ' . $type, view: $errorView);
}
return false;
}
App::init()
@@ -845,34 +869,34 @@ App::init()
->inject('request')
->inject('response')
->inject('log')
->inject('console')
->inject('project')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('locale')
->inject('localeCodes')
->inject('platforms')
->inject('geodb')
->inject('queueForStatsUsage')
->inject('queueForEvents')
->inject('queueForCertificates')
->inject('queueForFunctions')
->inject('executor')
->inject('platform')
->inject('isResourceBlocked')
->inject('previewHostname')
->inject('devKey')
->inject('apiKey')
->inject('httpReferrer')
->inject('httpReferrerSafe')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, array $platforms, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, Executor $executor, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, string $httpReferrer, string $httpReferrerSafe) {
->inject('cors')
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Func $queueForFunctions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
/*
* Appwrite Router
*/
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
$hostname = $request->getHostname() ?? '';
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if ($host !== $mainDomain || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -912,98 +936,12 @@ App::init()
}
}
$domain = $request->getHostname();
$domains = Config::getParam('domains', []);
if (!array_key_exists($domain, $domains)) {
$domain = new Domain(!empty($domain) ? $domain : '');
if (empty($domain->get()) || !$domain->isKnown() || $domain->isTest()) {
$domains[$domain->get()] = false;
Console::warning($domain->get() . ' is not a publicly accessible domain. Skipping SSL certificate generation.');
} elseif (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) {
Console::warning('Skipping SSL certificates generation on ACME challenge.');
} else {
Authorization::disable();
$envDomain = System::getEnv('_APP_DOMAIN', '');
$mainDomain = null;
if (!empty($envDomain) && $envDomain !== 'localhost') {
$mainDomain = $envDomain;
} else {
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$domainDocument = $dbForPlatform->getDocument('rules', md5($envDomain));
} else {
$domainDocument = $dbForPlatform->findOne('rules', [Query::orderAsc('$id')]);
}
$mainDomain = !$domainDocument->isEmpty() ? $domainDocument->getAttribute('domain') : $domain->get();
}
if ($mainDomain !== $domain->get()) {
Console::warning($domain->get() . ' is not a main domain. Skipping SSL certificate generation.');
} else {
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$domainDocument = $dbForPlatform->getDocument('rules', md5($domain->get()));
} else {
$domainDocument = $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain->get()])
]);
}
$owner = '';
$functionsDomainFallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', '');
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
$siteDomain = System::getEnv('_APP_DOMAIN_SITES', '');
if (!empty($functionsDomainFallback) && \str_ends_with($host, $functionsDomainFallback)) {
$functionsDomain = $functionsDomainFallback;
}
if (
(!empty($functionsDomain) && \str_ends_with($domain->get(), $functionsDomain)) ||
(!empty($siteDomain) && \str_ends_with($domain->get(), $siteDomain))
) {
$owner = 'Appwrite';
}
if ($domainDocument->isEmpty()) {
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
$domainDocument = new Document([
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
'$id' => $ruleId,
'domain' => $domain->get(),
'type' => 'api',
'status' => 'verifying',
'projectId' => $console->getId(),
'projectInternalId' => $console->getSequence(),
'search' => implode(' ', [$ruleId, $domain->get()]),
'owner' => $owner,
'region' => $console->getAttribute('region')
]);
$domainDocument = $dbForPlatform->createDocument('rules', $domainDocument);
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
$queueForCertificates
->setDomain($domainDocument)
->setSkipRenewCheck(true)
->trigger();
}
}
$domains[$domain->get()] = true;
Authorization::reset(); // ensure authorization is re-enabled
}
Config::setParam('domains', $domains);
}
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
if (\in_array($localeParam, $localeCodes)) {
$locale->setDefault($localeParam);
}
$origin = \parse_url($request->getOrigin($httpReferrer), PHP_URL_HOST);
$origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST);
$selfDomain = new Domain($request->getHostname());
$endDomain = new Domain((string)$origin);
Config::setParam(
@@ -1032,8 +970,8 @@ App::init()
$warnings = [];
/*
* Response format
*/
* Response format
*/
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
if (version_compare($responseFormat, '1.4.0', '<')) {
@@ -1053,14 +991,13 @@ App::init()
}
}
/*
* Security Headers
*
* As recommended at:
* @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers
*/
// Add Appwrite warning headers
if (!empty($warnings)) {
$response->addHeader('X-Appwrite-Warning', implode(';', $warnings));
}
if (System::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS
if ($request->getProtocol() !== 'https' && ($swooleRequest->header['host'] ?? '') !== 'localhost' && ($swooleRequest->header['host'] ?? '') !== APP_HOSTNAME_INTERNAL) { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations
if ($request->getProtocol() !== 'https' && ($swooleRequest->header['host'] ?? '') !== 'localhost') { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations
if ($request->getMethod() !== Request::METHOD_GET) {
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.');
}
@@ -1068,49 +1005,158 @@ App::init()
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
}
}
});
if ($request->getProtocol() === 'https') {
$response->addHeader('Strict-Transport-Security', 'max-age=' . (60 * 60 * 24 * 126)); // 126 days
/**
* Security headers
*
* @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers
*/
App::init()
->groups(['api', 'web'])
->inject('request')
->inject('response')
->inject('cors')
->inject('devKey')
->inject('originValidator')
->action(function (Request $request, Response $response, Cors $cors, Document $devKey, Validator $originValidator) {
// CORS headers
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
$response->addHeader($name, $value);
}
// Security headers
$response
->addHeader('Server', 'Appwrite')
->addHeader('X-Content-Type-Options', 'nosniff')
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Dev-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Forwarded-For, X-Forwarded-User-Agent')
->addHeader('Access-Control-Expose-Headers', 'X-Appwrite-Session, X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Origin', $httpReferrerSafe)
->addHeader('Access-Control-Allow-Credentials', 'true');
->addHeader('X-Content-Type-Options', 'nosniff');
if (!$devKey->isEmpty()) {
$response->addHeader('Access-Control-Allow-Origin', '*');
if ($request->getProtocol() === 'https') {
$maxAge = 60 * 60 * 24 * 126; // 126 days
$response->addHeader('Strict-Transport-Security', "max-age=$maxAge");
}
if (!empty($warnings)) {
$response->addHeader('X-Appwrite-Warning', implode(';', $warnings));
// Application level CSRF protection
$origin = $request->getOrigin();
if (empty($origin) || !$devKey->isEmpty() || !empty($request->getHeader('x-appwrite-key'))) {
return;
}
/*
* Validate Client Domain - Check to avoid CSRF attack
* Adding Appwrite API domains to allow XDOMAIN communication
* Skip this check for non-web platforms which are not required to send an origin header
*/
$origin = $request->getOrigin($request->getReferer(''));
$originValidator = new Origin($platforms);
if (
$devKey->isEmpty()
&& !empty($origin)
&& !$originValidator->isValid($origin)
&& \in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH, Request::METHOD_DELETE])
&& $route->getLabel('origin', false) !== '*'
&& empty($request->getHeader('x-appwrite-key', ''))
&& \parse_url($httpReferrerSafe, PHP_URL_HOST) === 'localhost'
) {
$route = $request->getRoute();
if ($route->getLabel('origin', false) === '*') {
return;
}
if (!$originValidator->isValid($origin)) {
throw new AppwriteException(AppwriteException::GENERAL_UNKNOWN_ORIGIN, $originValidator->getDescription());
}
});
/**
* Automatic certificate generation
*/
App::init()
->groups(['api', 'web'])
->inject('request')
->inject('console')
->inject('dbForPlatform')
->inject('queueForCertificates')
->inject('platform')
->inject('authorization')
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform, Authorization $authorization) {
$hostname = $request->getHostname();
$cache = Config::getParam('hostnames', []);
$platformHostnames = $platform['hostnames'] ?? [];
// 1. Cache hit
if (array_key_exists($hostname, $cache)) {
return;
}
// 2. Domain validation
$domain = new Domain(!empty($hostname) ? $hostname : '');
if (empty($domain->get()) || !$domain->isKnown() || $domain->isTest()) {
$cache[$domain->get()] = false;
Config::setParam('hostnames', $cache);
return;
}
if (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) {
return;
}
// 3. Check if domain is a main domain
if (!in_array($domain->get(), $platformHostnames)) {
return;
}
// 4. Check/create rule (requires DB access)
$authorization->skip(function () use ($dbForPlatform, $domain, $console, $queueForCertificates, &$cache) {
try {
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$document = $isMd5
? $dbForPlatform->getDocument('rules', md5($domain->get()))
: $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain->get()]),
]);
if (!$document->isEmpty()) {
return;
}
// 5. Create new rule
$owner = '';
// Mark owner as Appwrite if its appwrite-owned domain
$appwriteDomains = [];
$appwriteDomainEnvs = [
System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''),
System::getEnv('_APP_DOMAIN_FUNCTIONS', ''),
System::getEnv('_APP_DOMAIN_SITES', ''),
];
foreach ($appwriteDomainEnvs as $appwriteDomainEnv) {
foreach (\explode(',', $appwriteDomainEnv) as $appwriteDomain) {
if (empty($appwriteDomain)) {
continue;
}
$appwriteDomains[] = $appwriteDomain;
}
}
foreach ($appwriteDomains as $appwriteDomain) {
if (\str_ends_with($domain->get(), $appwriteDomain)) {
$owner = 'Appwrite';
break;
}
}
$ruleId = $isMd5 ? md5($domain->get()) : ID::unique();
$document = new Document([
'$id' => $ruleId,
'domain' => $domain->get(),
'type' => 'api',
'status' => 'verifying',
'projectId' => $console->getId(),
'projectInternalId' => $console->getSequence(),
'search' => implode(' ', [$ruleId, $domain->get()]),
'owner' => $owner,
'region' => $console->getAttribute('region')
]);
$dbForPlatform->createDocument('rules', $document);
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
$queueForCertificates
->setDomain($document)
->setSkipRenewCheck(true)
->trigger();
} catch (Duplicate $e) {
Console::info('Certificate already exists');
} finally {
$cache[$domain->get()] = true;
Config::setParam('hostnames', $cache);
}
});
});
App::options()
->inject('utopia')
->inject('swooleRequest')
@@ -1125,38 +1171,35 @@ App::options()
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
->inject('platform')
->inject('previewHostname')
->inject('project')
->inject('devKey')
->inject('apiKey')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey) {
->inject('cors')
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
/*
* Appwrite Router
*/
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if ($host !== $mainDomain || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
$origin = $request->getOrigin();
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
$response->addHeader($name, $value);
}
$response
->addHeader('Server', 'Appwrite')
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Dev-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Appwrite-Session, X-Fallback-Cookies, X-Forwarded-For, X-Forwarded-User-Agent')
->addHeader('Access-Control-Expose-Headers', 'X-Appwrite-Session, X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Origin', $origin)
->addHeader('Access-Control-Allow-Credentials', 'true')
->noContent();
if (!$devKey->isEmpty()) {
$response->addHeader('Access-Control-Allow-Origin', '*');
}
/** OPTIONS requests in utopia do not execute shutdown handlers, as a result we need to track the OPTIONS requests explicitly
* @see https://github.com/utopia-php/http/blob/0.33.16/src/App.php#L825-L855
*/
@@ -1178,7 +1221,8 @@ App::error()
->inject('log')
->inject('queueForStatsUsage')
->inject('devKey')
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, StatsUsage $queueForStatsUsage) {
->inject('authorization')
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, StatsUsage $queueForStatsUsage, Document $devKey, Authorization $authorization) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$route = $utopia->getRoute();
$class = \get_class($error);
@@ -1203,7 +1247,7 @@ App::error()
}
switch ($class) {
case 'Utopia\Exception':
case Utopia\Exception::class:
$error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, $code, $error);
switch ($code) {
case 400:
@@ -1214,10 +1258,10 @@ App::error()
break;
}
break;
case 'Utopia\Database\Exception\Authorization':
case Utopia\Database\Exception\Authorization::class:
$error = new AppwriteException(AppwriteException::USER_UNAUTHORIZED);
break;
case 'Utopia\Database\Exception\Timeout':
case Utopia\Database\Exception\Timeout::class:
$error = new AppwriteException(AppwriteException::DATABASE_TIMEOUT, previous: $error);
break;
}
@@ -1260,7 +1304,7 @@ App::error()
* If not a publishable error, track usage stats. Publishable errors are >= 500 or those explicitly marked as publish=true in errors.php
*/
if (!$publish && $project->getId() !== 'console') {
if (!DBUser::isPrivileged(Authorization::getRoles())) {
if (!DBUser::isPrivileged($authorization->getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
@@ -1322,7 +1366,87 @@ App::error()
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
$log->addExtra('roles', Authorization::getRoles());
$log->addExtra('roles', $authorization->getRoles());
try {
/* add queries to log */
$queries = $request->getParam('queries', []);
if (!empty($queries) && is_array($queries)) {
$parsedQueries = Query::parseQueries($queries);
// format query by removing sensitive values
$formatQuery = function (array $queryArray) use (&$formatQuery): ?array {
$method = $queryArray['method'] ?? '';
$values = $queryArray['values'] ?? [];
$attribute = $queryArray['attribute'] ?? '';
if (!is_string($method) || $method === '') {
return null;
}
// logical queries - recursively format nested queries
if (in_array($method, [Query::TYPE_AND, Query::TYPE_OR], true)) {
$nested = [];
foreach ($values as $nestedArray) {
if (is_array($nestedArray)) {
$formatted = $formatQuery($nestedArray);
if ($formatted !== null) {
$nested[] = $formatted;
}
}
}
return empty($nested) ? null : [$method => $nested];
}
// select - show selected attributes
if ($method === Query::TYPE_SELECT) {
$attributes = array_values(array_filter($values, 'is_string'));
return [$method => $attributes];
}
// pagination
if (in_array($method, [
Query::TYPE_LIMIT,
Query::TYPE_OFFSET,
Query::TYPE_CURSOR_AFTER,
Query::TYPE_CURSOR_BEFORE
], true)) {
return [$method => []];
}
// orders
if (in_array($method, [
Query::TYPE_ORDER_DESC,
Query::TYPE_ORDER_ASC,
Query::TYPE_ORDER_RANDOM
], true)) {
return [$method => !empty($attribute) ? [$attribute] : []];
}
// filter
if (!empty($attribute)) {
return [$method => [$attribute]];
}
// fallback
return [$method => []];
};
$formattedQueries = [];
foreach ($parsedQueries as $query) {
$formatted = $formatQuery($query->toArray());
if ($formatted !== null) {
$formattedQueries[] = $formatted;
}
}
if (!empty($formattedQueries)) {
$log->addExtra('queries', $formattedQueries);
}
}
} catch (Throwable $_) {
// don't fail the error handler
}
$action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD';
if (!empty($sdk)) {
@@ -1398,7 +1522,7 @@ App::error()
$template = $error->getView() ?? (($route) ? $route->getLabel('error', null) : null);
// TODO: Ideally use group 'api' here, but all wildcard routes seem to have 'api' at the moment
if (!\str_starts_with($route->getPath(), '/v1')) {
if (empty($route) || !\str_starts_with($route->getPath(), '/v1')) {
$template = __DIR__ . '/../views/general/error.phtml';
}
@@ -1443,18 +1567,19 @@ App::get('/robots.txt')
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
->inject('platform')
->inject('previewHostname')
->inject('apiKey')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) {
$host = $request->getHostname() ?? '';
$consoleDomain = System::getEnv('_APP_CONSOLE_DOMAIN', '');
$mainDomain = System::getEnv('_APP_DOMAIN', '');
if (($host === $consoleDomain || $host === $mainDomain || $host === 'localhost') && empty($previewHostname)) {
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1477,18 +1602,19 @@ App::get('/humans.txt')
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
->inject('platform')
->inject('previewHostname')
->inject('apiKey')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) {
$host = $request->getHostname() ?? '';
$consoleDomain = System::getEnv('_APP_CONSOLE_DOMAIN', '');
$mainDomain = System::getEnv('_APP_DOMAIN', '');
if (($host === $consoleDomain || $host === $mainDomain || $host === 'localhost') && empty($previewHostname)) {
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1572,7 +1698,8 @@ App::get('/v1/ping')
->inject('project')
->inject('dbForPlatform')
->inject('queueForEvents')
->action(function (Response $response, Document $project, Database $dbForPlatform, Event $queueForEvents) {
->inject('authorization')
->action(function (Response $response, Document $project, Database $dbForPlatform, Event $queueForEvents, Authorization $authorization) {
if ($project->isEmpty() || $project->getId() === 'console') {
throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND);
}
@@ -1584,7 +1711,7 @@ App::get('/v1/ping')
->setAttribute('pingCount', $pingCount)
->setAttribute('pingedAt', $pingedAt);
Authorization::skip(function () use ($dbForPlatform, $project) {
$authorization->skip(function () use ($dbForPlatform, $project) {
$dbForPlatform->updateDocument('projects', $project->getId(), $project);
});
+28 -6
View File
@@ -15,7 +15,6 @@ use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\UID;
use Utopia\Locale\Locale;
use Utopia\System\System;
use Utopia\Validator\Host;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
@@ -27,7 +26,7 @@ App::get('/v1/mock/tests/general/oauth2')
->label('docs', false)
->label('mock', true)
->param('client_id', '', new Text(100), 'OAuth2 Client ID.')
->param('redirect_uri', '', new Host(['localhost']), 'OAuth2 Redirect URI.') // Important to deny an open redirect attack
->param('redirect_uri', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator']) // Important to deny an open redirect attack
->param('scope', '', new Text(100), 'OAuth2 scope list.')
->param('state', '', new Text(1024), 'OAuth2 state.')
->inject('response')
@@ -64,7 +63,7 @@ App::get('/v1/mock/tests/general/oauth2/token')
->param('client_id', '', new Text(100), 'OAuth2 Client ID.')
->param('client_secret', '', new Text(100), 'OAuth2 scope list.')
->param('grant_type', 'authorization_code', new WhiteList(['refresh_token', 'authorization_code']), 'OAuth2 Grant Type.', true)
->param('redirect_uri', '', new Host(['localhost']), 'OAuth2 Redirect URI.', true)
->param('redirect_uri', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
->param('code', '', new Text(100), 'OAuth2 state.', true)
->param('refresh_token', '', new Text(100), 'OAuth2 refresh token.', true)
->inject('response')
@@ -118,6 +117,28 @@ App::get('/v1/mock/tests/general/oauth2/user')
'id' => 1,
'name' => 'User Name',
'email' => 'useroauth@localhost.test',
'verified' => true,
]);
});
App::get('/v1/mock/tests/general/oauth2/user-unverified')
->desc('OAuth2 User Unverified')
->groups(['mock'])
->label('scope', 'public')
->label('docs', false)
->param('token', '', new Text(100), 'OAuth2 Access Token.')
->inject('response')
->action(function (string $token, Response $response) {
if ($token != '123456') {
throw new Exception(Exception::GENERAL_MOCK, 'Invalid token');
}
$response->json([
'id' => 2,
'name' => 'User Name Unverified',
'email' => 'useroauthunverified@localhost.test',
'verified' => false,
]);
});
@@ -170,7 +191,7 @@ App::post('/v1/mock/api-key-unprefixed')
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$scopes = array_keys(Config::getParam('scopes'));
$scopes = array_keys(Config::getParam('projectScopes'));
$key = new Document([
'$id' => ID::unique(),
@@ -179,8 +200,9 @@ App::post('/v1/mock/api-key-unprefixed')
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'resourceInternalId' => $project->getSequence(),
'resourceId' => $project->getId(),
'resourceType' => 'projects',
'name' => 'Outdated key',
'scopes' => $scopes,
'expire' => null,
+150 -226
View File
@@ -8,12 +8,14 @@ use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Realtime;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Functions\EventProcessor;
use Appwrite\SDK\Method;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
@@ -28,7 +30,7 @@ use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Queue\Publisher;
use Utopia\Database\Validator\Authorization\Input;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Validator\WhiteList;
@@ -72,151 +74,6 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
return $label;
};
/**
* This isolated event handling for `users.*.create` which is based on a `Database::EVENT_DOCUMENT_CREATE` listener may look odd, but it is **intentional**.
*
* Accounts can be created in many ways beyond `createAccount`
* (anonymous, OAuth, phone, etc.), and those flows are probably not covered in event tests; so we handle this here.
*/
$eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) {
// Only trigger events for user creation with the database listener.
if ($document->getCollection() !== 'users') {
return;
}
$queueForEvents
->setEvent('users.[userId].create')
->setParam('userId', $document->getId())
->setPayload($response->output($document, Response::MODEL_USER));
// Trigger functions, webhooks, and realtime events
$queueForFunctions
->from($queueForEvents)
->trigger();
/** Trigger webhooks events only if a project has them enabled */
if (!empty($project->getAttribute('webhooks'))) {
$queueForWebhooks
->from($queueForEvents)
->trigger();
}
/** Trigger realtime events only for non console events */
if ($queueForEvents->getProject()->getId() !== 'console') {
$queueForRealtime
->from($queueForEvents)
->trigger();
}
};
$usageDatabaseListener = function (string $event, Document $document, StatsUsage $queueForStatsUsage) {
$value = 1;
switch ($event) {
case Database::EVENT_DOCUMENT_DELETE:
$value = -1;
break;
case Database::EVENT_DOCUMENTS_DELETE:
$value = -1 * $document->getAttribute('modified', 0);
break;
case Database::EVENT_DOCUMENTS_CREATE:
$value = $document->getAttribute('modified', 0);
break;
case Database::EVENT_DOCUMENTS_UPSERT:
$value = $document->getAttribute('created', 0);
break;
}
switch (true) {
case $document->getCollection() === 'teams':
$queueForStatsUsage->addMetric(METRIC_TEAMS, $value); // per project
break;
case $document->getCollection() === 'users':
$queueForStatsUsage->addMetric(METRIC_USERS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage->addReduce($document);
}
break;
case $document->getCollection() === 'sessions': // sessions
$queueForStatsUsage->addMetric(METRIC_SESSIONS, $value); //per project
break;
case $document->getCollection() === 'databases': // databases
$queueForStatsUsage->addMetric(METRIC_DATABASES, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage->addReduce($document);
}
break;
case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$queueForStatsUsage
->addMetric(METRIC_COLLECTIONS, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value);
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage->addReduce($document);
}
break;
case str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_'): //documents
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$collectionInternalId = $parts[3] ?? 0;
$queueForStatsUsage
->addMetric(METRIC_DOCUMENTS, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), $value) // per database
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), $value); // per collection
break;
case $document->getCollection() === 'buckets': //buckets
$queueForStatsUsage
->addMetric(METRIC_BUCKETS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage
->addReduce($document);
}
break;
case str_starts_with($document->getCollection(), 'bucket_'): // files
$parts = explode('_', $document->getCollection());
$bucketInternalId = $parts[1];
$queueForStatsUsage
->addMetric(METRIC_FILES, $value) // per project
->addMetric(METRIC_FILES_STORAGE, $document->getAttribute('sizeOriginal') * $value) // per project
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES), $value) // per bucket
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES_STORAGE), $document->getAttribute('sizeOriginal') * $value); // per bucket
break;
case $document->getCollection() === 'functions':
$queueForStatsUsage
->addMetric(METRIC_FUNCTIONS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage
->addReduce($document);
}
break;
case $document->getCollection() === 'sites':
$queueForStatsUsage
->addMetric(METRIC_SITES, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage
->addReduce($document);
}
break;
case $document->getCollection() === 'deployments':
$queueForStatsUsage
->addMetric(METRIC_DEPLOYMENTS, $value) // per project
->addMetric(METRIC_DEPLOYMENTS_STORAGE, $document->getAttribute('size') * $value) // per project
->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS), $value) // per function
->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value)
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS), $value) // per function
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value);
break;
default:
break;
}
};
App::init()
->groups(['api'])
->inject('utopia')
@@ -231,7 +88,8 @@ App::init()
->inject('mode')
->inject('team')
->inject('apiKey')
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey) {
->inject('authorization')
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
$route = $utopia->getRoute();
/**
@@ -299,10 +157,6 @@ App::init()
// Step 5: API Key Authentication
if (!empty($apiKey)) {
// Verify no user session exists simultaneously
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
}
// Check if key is expired
if ($apiKey->isExpired()) {
throw new Exception(Exception::PROJECT_KEY_EXPIRED);
@@ -316,7 +170,7 @@ App::init()
// Handle special app role case
if ($apiKey->getRole() === User::ROLE_APPS) {
// Disable authorization checks for API keys
Authorization::setDefaultStatus(false);
$authorization->setDefaultStatus(false);
$user = new User([
'$id' => '',
@@ -331,23 +185,38 @@ App::init()
}
// For standard keys, update last accessed time
if ($apiKey->getType() === API_KEY_STANDARD) {
$dbKey = $project->find(
key: 'secret',
find: $request->getHeader('x-appwrite-key', ''),
subject: 'keys'
);
if (\in_array($apiKey->getType(), [API_KEY_STANDARD, API_KEY_ORGANIZATION, API_KEY_ACCOUNT])) {
$dbKey = null;
if (!empty($apiKey->getProjectId())) {
$dbKey = $project->find(
key: 'secret',
find: $request->getHeader('x-appwrite-key', ''),
subject: 'keys'
);
} elseif (!empty($apiKey->getUserId())) {
$dbKey = $user->find(
key: 'secret',
find: $request->getHeader('x-appwrite-key', ''),
subject: 'keys'
);
} elseif (!empty($apiKey->getTeamId())) {
$dbKey = $team->find(
key: 'secret',
find: $request->getHeader('x-appwrite-key', ''),
subject: 'keys'
);
}
if (!$dbKey) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$updates = new Document();
$accessedAt = $dbKey->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
$dbKey->setAttribute('accessedAt', DateTime::now());
$dbForPlatform->updateDocument('keys', $dbKey->getId(), $dbKey);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$updates->setAttribute('accessedAt', DateTime::now());
}
$sdkValidator = new WhiteList($servers, true);
@@ -358,12 +227,21 @@ App::init()
if (!in_array($sdk, $sdks)) {
$sdks[] = $sdk;
$dbKey->setAttribute('sdks', $sdks);
/** Update access time as well */
$dbKey->setAttribute('accessedAt', Datetime::now());
$dbForPlatform->updateDocument('keys', $dbKey->getId(), $dbKey);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$updates->setAttribute('sdks', $sdks);
$updates->setAttribute('accessedAt', Datetime::now());
}
}
if (!$updates->isEmpty()) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->updateDocument('keys', $dbKey->getId(), $updates));
if (!empty($apiKey->getProjectId())) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
} elseif (!empty($apiKey->getUserId())) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('users', $user->getId()));
} elseif (!empty($apiKey->getTeamId())) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('teams', $team->getId()));
}
}
@@ -390,14 +268,14 @@ App::init()
$scopes = \array_merge($scopes, $roles[$role]['scopes']);
}
Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users.
$authorization->setDefaultStatus(false); // Cancel security segmentation for admin users.
}
$scopes = \array_unique($scopes);
Authorization::setRole($role);
foreach ($user->getRoles() as $authRole) {
Authorization::setRole($authRole);
$authorization->addRole($role);
foreach ($user->getRoles($authorization) as $authRole) {
$authorization->addRole($authRole);
}
// Step 6: Update project and user last activity
@@ -405,7 +283,7 @@ App::init()
$accessedAt = $project->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$project->setAttribute('accessedAt', DateTime::now());
Authorization::skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
}
}
@@ -440,7 +318,7 @@ App::init()
if (
array_key_exists($namespace, $project->getAttribute('services', []))
&& !$project->getAttribute('services', [])[$namespace]
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
) {
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
}
@@ -485,9 +363,6 @@ App::init()
->inject('response')
->inject('project')
->inject('user')
->inject('publisher')
->inject('publisherFunctions')
->inject('publisherWebhooks')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('queueForAudits')
@@ -495,6 +370,8 @@ App::init()
->inject('queueForDatabase')
->inject('queueForBuilds')
->inject('queueForStatsUsage')
->inject('queueForFunctions')
->inject('queueForMails')
->inject('dbForProject')
->inject('timelimit')
->inject('resourceToken')
@@ -503,14 +380,16 @@ App::init()
->inject('plan')
->inject('devKey')
->inject('telemetry')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry) use ($usageDatabaseListener, $eventDatabaseListener) {
->inject('platform')
->inject('authorization')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
$route = $utopia->getRoute();
if (
array_key_exists('rest', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['rest']
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
@@ -540,7 +419,7 @@ App::init()
$closestLimit = null;
$roles = Authorization::getRoles();
$roles = $authorization->getRoles();
$isPrivilegedUser = User::isPrivileged($roles);
$isAppUser = User::isApp($roles);
@@ -577,6 +456,10 @@ App::init()
}
}
/**
* TODO: (@loks0n)
* Avoid mutating the message across file boundaries - it's difficult to reason about at scale.
*/
/*
* Background Jobs
*/
@@ -607,43 +490,28 @@ App::init()
}
}
/* Auto-set projects */
$queueForDeletes->setProject($project);
$queueForDatabase->setProject($project);
$queueForBuilds->setProject($project);
$queueForMessaging->setProject($project);
$queueForFunctions->setProject($project);
$queueForBuilds->setProject($project);
// Clone the queues, to prevent events triggered by the database listener
// from overwriting the events that are supposed to be triggered in the shutdown hook.
$queueForEventsClone = new Event($publisher);
$queueForFunctions = new Func($publisherFunctions);
$queueForWebhooks = new Webhook($publisherWebhooks);
$queueForRealtime = new Realtime();
/* Auto-set platforms */
$queueForFunctions->setPlatform($platform);
$queueForBuilds->setPlatform($platform);
$queueForMails->setPlatform($platform);
$dbForProject
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENTS_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENTS_UPSERT, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENT_CREATE, 'create-trigger-events', fn ($event, $document) => $eventDatabaseListener(
$project,
$document,
$response,
$queueForEventsClone->from($queueForEvents),
$queueForFunctions->from($queueForEvents),
$queueForWebhooks->from($queueForEvents),
$queueForRealtime->from($queueForEvents)
));
$useCache = $route->getLabel('cache', false);
$storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load');
if ($useCache) {
$route = $utopia->match($request);
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !User::isPrivileged(Authorization::getRoles());
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !User::isPrivileged($authorization->getRoles());
$key = $request->cacheIdentifier();
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
);
@@ -660,10 +528,10 @@ App::init()
if ($type === 'bucket' && (!$isImageTransformation || !$isDisabled)) {
$bucketId = $parts[1] ?? null;
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAppUser && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -674,8 +542,7 @@ App::init()
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_READ);
$valid = $validator->isValid($bucket->getRead());
$valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead()));
if (!$fileSecurity && !$valid && !$isToken) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
@@ -686,7 +553,7 @@ App::init()
if ($fileSecurity && !$valid && !$isToken) {
$file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId);
} else {
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
$file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
}
if (!$resourceToken->isEmpty() && $resourceToken->getAttribute('fileInternalId') !== $file->getSequence()) {
@@ -697,11 +564,11 @@ App::init()
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
//Do not update transformedAt if it's a console user
if (!User::isPrivileged(Authorization::getRoles())) {
if (!User::isPrivileged($authorization->getRoles())) {
$transformedAt = $file->getAttribute('transformedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
$file->setAttribute('transformedAt', DateTime::now());
Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file));
$authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file));
}
}
}
@@ -797,7 +664,10 @@ App::shutdown()
->inject('queueForWebhooks')
->inject('queueForRealtime')
->inject('dbForProject')
->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) {
->inject('authorization')
->inject('timelimit')
->inject('eventProcessor')
->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -806,9 +676,15 @@ App::shutdown()
$queueForEvents->setPayload($responsePayload);
}
$queueForFunctions
->from($queueForEvents)
->trigger();
// Get project and function/webhook events (cached)
$functionsEvents = $eventProcessor->getFunctionsEvents($project, $dbForProject);
$webhooksEvents = $eventProcessor->getWebhooksEvents($project);
// Generate events for this operation
$generatedEvents = Event::generateEvents(
$queueForEvents->getEvent(),
$queueForEvents->getParams()
);
if ($project->getId() !== 'console') {
$queueForRealtime
@@ -816,21 +692,69 @@ App::shutdown()
->trigger();
}
/** Trigger webhooks events only if a project has them enabled
* A future optimisation is to only trigger webhooks if the webhook is "enabled"
* But it might have performance implications on the API due to the number of webhooks etc.
* Some profiling is needed to see if this is a problem.
*/
if (!empty($project->getAttribute('webhooks'))) {
$queueForWebhooks
->from($queueForEvents)
->trigger();
// Only trigger functions if there are matching function events
if (!empty($functionsEvents)) {
foreach ($generatedEvents as $event) {
if (isset($functionsEvents[$event])) {
$queueForFunctions
->from($queueForEvents)
->trigger();
break;
}
}
}
// Only trigger webhooks if there are matching webhook events
if (!empty($webhooksEvents)) {
foreach ($generatedEvents as $event) {
if (isset($webhooksEvents[$event])) {
$queueForWebhooks
->from($queueForEvents)
->trigger();
break;
}
}
}
}
$route = $utopia->getRoute();
$requestParams = $route->getParamsValues();
/**
* Abuse labels
*/
$abuseEnabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled';
$abuseResetCode = $route->getLabel('abuse-reset', []);
$abuseResetCode = \is_array($abuseResetCode) ? $abuseResetCode : [$abuseResetCode];
if ($abuseEnabled && \count($abuseResetCode) > 0 && \in_array($response->getStatusCode(), $abuseResetCode)) {
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
$abuseKeyLabel = (!is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
foreach ($abuseKeyLabel as $abuseKey) {
$start = $request->getContentRangeStart();
$end = $request->getContentRangeEnd();
$timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600));
$timeLimit
->setParam('{projectId}', $project->getId())
->setParam('{userId}', $user->getId())
->setParam('{userAgent}', $request->getUserAgent(''))
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getHostname() . $route->getPath())
->setParam('{method}', $request->getMethod())
->setParam('{chunkId}', (int)($start / ($end + 1 - $start)));
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
if (!empty($value)) {
$timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
}
}
$abuse = new Abuse($timeLimit);
$abuse->reset();
}
}
/**
* Audit labels
*/
@@ -923,11 +847,11 @@ App::shutdown()
$key = $request->cacheIdentifier();
$signature = md5($data['payload']);
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
$accessedAt = $cacheLog->getAttribute('accessedAt', 0);
$now = DateTime::now();
if ($cacheLog->isEmpty()) {
Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([
$authorization->skip(fn () => $dbForProject->createDocument('cache', new Document([
'$id' => $key,
'resource' => $resource,
'resourceType' => $resourceType,
@@ -937,7 +861,7 @@ App::shutdown()
])));
} elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
$cacheLog->setAttribute('accessedAt', $now);
Authorization::skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog));
$authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog));
// Overwrite the file every APP_CACHE_UPDATE seconds to update the file modified time that is used in the TTL checks in cache->load()
$cache->save($key, $data['payload']);
}
@@ -949,7 +873,7 @@ App::shutdown()
}
if ($project->getId() !== 'console') {
if (!User::isPrivileged(Authorization::getRoles())) {
if (!User::isPrivileged($authorization->getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
+4 -3
View File
@@ -36,7 +36,8 @@ App::init()
->inject('request')
->inject('project')
->inject('geodb')
->action(function (App $utopia, Request $request, Document $project, Reader $geodb) {
->inject('authorization')
->action(function (App $utopia, Request $request, Document $project, Reader $geodb, Authorization $authorization) {
$denylist = System::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', '');
if (!empty($denylist && $project->getId() === 'console')) {
$countries = explode(',', $denylist);
@@ -49,8 +50,8 @@ App::init()
$route = $utopia->match($request);
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$isAppUser = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
$isAppUser = User::isApp($authorization->getRoles());
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
return;
-43
View File
@@ -1,43 +0,0 @@
<?php
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\App;
App::init()
->groups(['web'])
->inject('request')
->inject('response')
->action(function (Request $request, Response $response) {
$response
->addHeader('X-Frame-Options', 'SAMEORIGIN') // Avoid console and homepage from showing in iframes
->addHeader('X-XSS-Protection', '1; mode=block; report=/v1/xss?url=' . \urlencode($request->getURI()))
->addHeader('X-UA-Compatible', 'IE=Edge') // Deny IE browsers from going into quirks mode
;
});
App::get('/')
->alias('auth/*')
->alias('/invite')
->alias('/login')
->alias('/mfa')
->alias('/card/*')
->alias('/recover')
->alias('/register/*')
->groups(['web'])
->label('permission', 'public')
->label('scope', 'home')
->inject('request')
->inject('response')
->action(function (Request $request, Response $response) {
$url = parse_url($request->getURI());
$target = "/console{$url['path']}";
$params = $request->getParams();
if (!empty($params)) {
$target .= "?" . \http_build_query($params);
}
if ($url['fragment'] ?? false) {
$target .= "#{$url['fragment']}";
}
$response->redirect($target);
});
+1 -1
View File
@@ -10,7 +10,7 @@ App::get('/versions')
->label('scope', 'public')
->inject('response')
->action(function (Response $response) {
$platforms = Config::getParam('platforms');
$platforms = Config::getParam('sdks');
$versions = [
'server' => APP_VERSION_STABLE,
+60 -18
View File
@@ -12,6 +12,8 @@ use Swoole\Process;
use Swoole\Table;
use Swoole\Timer;
use Utopia\App;
use Utopia\Audit\Adapter\Database as AdapterDatabase;
use Utopia\Audit\Adapter\SQL as AuditAdapterSQL;
use Utopia\Audit\Audit;
use Utopia\CLI\Console;
use Utopia\Compression\Compression;
@@ -25,7 +27,6 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Logger\Log\User;
use Utopia\Pools\Group;
@@ -99,11 +100,19 @@ function dispatch(Server $server, int $fd, int $type, $data = null): int
$risky = false;
if (str_starts_with($request, 'POST') && str_contains($request, '/executions')) {
$risky = true;
} elseif (str_ends_with($domain, System::getEnv('_APP_DOMAIN_FUNCTIONS'))) {
$risky = true;
} elseif ($domains->get(md5($domain), 'value') === 1) {
// executions request coming from custom domain
$risky = true;
} else {
foreach (\explode(',', System::getEnv('_APP_DOMAIN_FUNCTIONS')) as $riskyDomain) {
if (empty($riskyDomain)) {
continue;
}
if (str_ends_with($domain, $riskyDomain)) {
$risky = true;
break;
}
}
}
if ($risky) {
@@ -259,9 +268,12 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
createDatabase($app, 'getLogsDB', 'logs', $collections['logs'], $pools);
// create appwrite database, `dbForPlatform` is a direct access call.
createDatabase($app, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections) {
if ($dbForPlatform->getCollection(Audit::COLLECTION)->isEmpty()) {
$audit = new Audit($dbForPlatform);
createDatabase($app, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections, $app) {
$authorization = $app->getResource('authorization');
if ($dbForPlatform->getCollection(AuditAdapterSQL::COLLECTION)->isEmpty()) {
$adapter = new AdapterDatabase($dbForPlatform);
$audit = new Audit($adapter);
$audit->setup();
}
@@ -318,9 +330,9 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
$dbForPlatform->createCollection('bucket_' . $bucket->getSequence(), $attributes, $indexes);
}
if (Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')->isEmpty())) {
if ($authorization->skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')->isEmpty())) {
Console::info(" └── Creating screenshots bucket...");
Authorization::skip(fn () => $dbForPlatform->createDocument('buckets', new Document([
$authorization->skip(fn () => $dbForPlatform->createDocument('buckets', new Document([
'$id' => ID::custom('screenshots'),
'$collection' => ID::custom('buckets'),
'name' => 'Screenshots',
@@ -335,7 +347,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
'search' => 'buckets Screenshots',
])));
$bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots'));
$bucket = $authorization->skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots'));
Console::info(" └── Creating files collection for screenshots bucket...");
$files = $collections['buckets']['files'] ?? [];
@@ -363,7 +375,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
'orders' => $index['orders'],
]), $files['indexes']);
Authorization::skip(fn () => $dbForPlatform->createCollection('bucket_' . $bucket->getSequence(), $attributes, $indexes));
$authorization->skip(fn () => $dbForPlatform->createCollection('bucket_' . $bucket->getSequence(), $attributes, $indexes));
}
});
@@ -389,8 +401,9 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
Console::success('[Setup] - Skip: metadata table already exists');
}
if ($dbForProject->getCollection(Audit::COLLECTION)->isEmpty()) {
$audit = new Audit($dbForProject);
if ($dbForProject->getCollection(AuditAdapterSQL::COLLECTION)->isEmpty()) {
$adapter = new AdapterDatabase($dbForProject);
$audit = new Audit($adapter);
$audit->setup();
}
@@ -454,8 +467,12 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
App::setResource('pools', fn () => $pools);
try {
Authorization::cleanRoles();
Authorization::setRole(Role::any()->toString());
$authorization = $app->getResource('authorization');
$request->setAuthorization($authorization);
$response->setAuthorization($authorization);
$authorization->cleanRoles();
$authorization->addRole(Role::any()->toString());
$app->run($request, $response);
} catch (\Throwable $th) {
@@ -497,7 +514,7 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
$log->addExtra('file', $th->getFile());
$log->addExtra('line', $th->getLine());
$log->addExtra('trace', $th->getTraceAsString());
$log->addExtra('roles', Authorization::getRoles());
$log->addExtra('roles', isset($authorization) ? $authorization->getRoles() : []);
$sdk = $route->getLabel("sdk", false);
@@ -556,7 +573,7 @@ $http->on(Constant::EVENT_TASK, function () use ($register, $domains) {
/** @var Utopia\Database\Database $dbForPlatform */
$dbForPlatform = $app->getResource('dbForPlatform');
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $domains, &$lastSyncUpdate) {
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $domains, &$lastSyncUpdate, $app) {
try {
$time = DateTime::now();
$limit = 1000;
@@ -573,7 +590,8 @@ $http->on(Constant::EVENT_TASK, function () use ($register, $domains) {
}
$results = [];
try {
$results = Authorization::skip(fn () => $dbForPlatform->find('rules', $queries));
$authorization = $app->getResource('authorization');
$results = $authorization->skip(fn () => $dbForPlatform->find('rules', $queries));
} catch (Throwable $th) {
Console::error($th->getMessage());
}
@@ -581,9 +599,33 @@ $http->on(Constant::EVENT_TASK, function () use ($register, $domains) {
$sum = count($results);
foreach ($results as $document) {
$domain = $document->getAttribute('domain');
if (str_ends_with($domain, System::getEnv('_APP_DOMAIN_FUNCTIONS')) || str_ends_with($domain, System::getEnv('_APP_DOMAIN_SITES'))) {
$denyDomains = [];
$denyEnvVars = [
System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''),
System::getEnv('_APP_DOMAIN_FUNCTIONS', ''),
System::getEnv('_APP_DOMAIN_SITES', ''),
];
foreach ($denyEnvVars as $denyEnvVar) {
foreach (\explode(',', $denyEnvVar) as $denyDomain) {
if (empty($denyDomain)) {
continue;
}
$denyDomains[] = $denyDomain;
}
}
$isDenyDomain = false;
foreach ($denyDomains as $denyDomain) {
if (str_ends_with($domain, $denyDomain)) {
$isDenyDomain = true;
}
}
if ($isDenyDomain) {
continue;
}
$domains->set(md5($domain), ['value' => 1]);
}
$latestDocument = !empty(array_key_last($results)) ? $results[array_key_last($results)] : null;
+1
View File
@@ -26,6 +26,7 @@ require_once __DIR__ . '/init/database/filters.php';
require_once __DIR__ . '/init/database/formats.php';
require_once __DIR__ . '/init/locales.php';
require_once __DIR__ . '/init/registers.php';
require_once __DIR__ . '/init/models.php';
require_once __DIR__ . '/init/resources.php';
\stream_context_set_default([ // Set global user agent and http settings
+5 -2
View File
@@ -15,13 +15,16 @@ 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('sdks', __DIR__ . '/../config/sdks.php', $configAdapter);
Config::load('platform', __DIR__ . '/../config/platform.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('usage', __DIR__ . '/../config/usage.php', $configAdapter);
Config::load('roles', __DIR__ . '/../config/roles.php', $configAdapter); // User roles and scopes
Config::load('scopes', __DIR__ . '/../config/scopes.php', $configAdapter); // User roles and scopes
Config::load('projectScopes', __DIR__ . '/../config/scopes/project.php', $configAdapter);
Config::load('organizationScopes', __DIR__ . '/../config/scopes/organization.php', $configAdapter);
Config::load('accountScopes', __DIR__ . '/../config/scopes/account.php', $configAdapter);
Config::load('services', __DIR__ . '/../config/services.php', $configAdapter); // List of services
Config::load('variables', __DIR__ . '/../config/variables.php', $configAdapter); // List of env variables
Config::load('regions', __DIR__ . '/../config/regions.php', $configAdapter); // List of available regions
+30 -6
View File
@@ -4,12 +4,17 @@ use Appwrite\Platform\Modules\Compute\Specification;
const APP_NAME = 'Appwrite';
const APP_DOMAIN = 'appwrite.io';
// Email
const APP_EMAIL_TEAM = 'team@localhost.test'; // Default email address
const APP_EMAIL_SECURITY = ''; // Default security email address
const APP_EMAIL_LOGO_URL = 'https://cloud.appwrite.io/images/mails/logo.png';
const APP_EMAIL_ACCENT_COLOR = '#fd366e';
const APP_EMAIL_TERMS_URL = 'https://appwrite.io/terms';
const APP_EMAIL_PRIVACY_URL = 'https://appwrite.io/privacy';
const APP_EMAIL_PLATFORM_NAME = 'Appwrite';
const APP_EMAIL_FOOTER_IMAGE_URL = 'https://appwrite.io/email/footer.png';
const APP_USERAGENT = APP_NAME . '-Server v%s. Please report abuse at %s';
const APP_MODE_DEFAULT = 'default';
const APP_MODE_ADMIN = 'admin';
@@ -39,7 +44,7 @@ const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours
const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 4321;
const APP_VERSION_STABLE = '1.8.0';
const APP_VERSION_STABLE = '1.8.1';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
@@ -55,6 +60,7 @@ const APP_DATABASE_TIMEOUT_MILLISECONDS_API = 15 * 1000; // 15 seconds
const APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER = 300 * 1000; // 5 minutes
const APP_DATABASE_TIMEOUT_MILLISECONDS_TASK = 300 * 1000; // 5 minutes
const APP_DATABASE_QUERY_MAX_VALUES = 500;
const APP_DATABASE_QUERY_MAX_VALUES_WORKER = 5000;
const APP_DATABASE_ENCRYPT_SIZE_MIN = 150;
const APP_DATABASE_TXN_TTL_MIN = 60; // 1 minute
const APP_DATABASE_TXN_TTL_MAX = 3600; // 1 hour
@@ -81,13 +87,12 @@ const APP_SOCIAL_DISCORD_CHANNEL = '564160730845151244';
const APP_SOCIAL_DEV = 'https://dev.to/appwrite';
const APP_SOCIAL_STACKSHARE = 'https://stackshare.io/appwrite';
const APP_SOCIAL_YOUTUBE = 'https://www.youtube.com/c/appwrite?sub_confirmation=1';
const APP_HOSTNAME_INTERNAL = 'appwrite';
const APP_COMPUTE_CPUS_DEFAULT = 0.5;
const APP_COMPUTE_MEMORY_DEFAULT = 512;
const APP_COMPUTE_SPECIFICATION_DEFAULT = Specification::S_1VCPU_512MB;
const APP_PLATFORM_SERVER = 'server';
const APP_PLATFORM_CLIENT = 'client';
const APP_PLATFORM_CONSOLE = 'console';
const APP_SDK_PLATFORM_SERVER = 'server';
const APP_SDK_PLATFORM_CLIENT = 'client';
const APP_SDK_PLATFORM_CONSOLE = 'console';
const APP_VCS_GITHUB_USERNAME = 'Appwrite';
const APP_VCS_GITHUB_EMAIL = 'team@appwrite.io';
const APP_VCS_GITHUB_URL = 'https://github.com/TeamAppwrite';
@@ -156,6 +161,9 @@ const ACTIVITY_TYPE_GUEST = 'guest';
const MFA_RECENT_DURATION = 1800; // 30 mins
// Database name
const APP_DATABASE = 'appwrite';
// Database Reconnect
const DATABASE_RECONNECT_SLEEP = 2;
const DATABASE_RECONNECT_MAX_ATTEMPTS = 10;
@@ -173,8 +181,10 @@ const BUILD_TYPE_DEPLOYMENT = 'deployment';
const BUILD_TYPE_RETRY = 'retry';
// Deletion Types
const DELETE_TYPE_DATABASES = 'databases';
const ENABLE_EXECUTIONS_LIMIT_ON_ROUTE = false;
const DELETE_TYPE_DATABASES = 'databases';
const DELETE_TYPE_DOCUMENT = 'document';
const DELETE_TYPE_COLLECTIONS = 'collections';
const DELETE_TYPE_TRANSACTION = 'transaction';
@@ -184,8 +194,10 @@ const DELETE_TYPE_SITES = 'sites';
const DELETE_TYPE_FUNCTIONS = 'functions';
const DELETE_TYPE_DEPLOYMENTS = 'deployments';
const DELETE_TYPE_USERS = 'users';
const DELETE_TYPE_TEAMS = 'teams';
const DELETE_TYPE_TEAM_PROJECTS = 'teams_projects';
const DELETE_TYPE_EXECUTIONS = 'executions';
const DELETE_TYPE_EXECUTIONS_LIMIT = 'executionsLimit';
const DELETE_TYPE_AUDIT = 'audit';
const DELETE_TYPE_ABUSE = 'abuse';
const DELETE_TYPE_USAGE = 'usage';
@@ -204,6 +216,12 @@ const DELETE_TYPE_SESSION_TARGETS = 'session_targets';
const DELETE_TYPE_CSV_EXPORTS = 'csv_exports';
const DELETE_TYPE_MAINTENANCE = 'maintenance';
// Rule statuses
const RULE_STATUS_CREATED = 'created'; // This is also the status when domain DNS verification fails.
const RULE_STATUS_CERTIFICATE_GENERATING = 'verifying';
const RULE_STATUS_CERTIFICATE_GENERATION_FAILED = 'unverified';
const RULE_STATUS_VERIFIED = 'verified';
// Message types
const MESSAGE_SEND_TYPE_INTERNAL = 'internal';
const MESSAGE_SEND_TYPE_EXTERNAL = 'external';
@@ -232,6 +250,8 @@ const MESSAGE_TYPE_PUSH = 'push';
// API key types
const API_KEY_STANDARD = 'standard';
const API_KEY_DYNAMIC = 'dynamic';
const API_KEY_ORGANIZATION = 'organization';
const API_KEY_ACCOUNT = 'account';
// Usage metrics
const METRIC_TEAMS = 'teams';
const METRIC_USERS = 'users';
@@ -363,3 +383,7 @@ const SCHEDULE_RESOURCE_TYPE_MESSAGE = 'message';
/** Preview cookie */
const COOKIE_NAME_PREVIEW = 'a_jwt_console';
// Cache Reconnect
const CACHE_RECONNECT_MAX_RETRIES = 2;
const CACHE_RECONNECT_RETRY_DELAY = 1000;
+56 -25
View File
@@ -4,7 +4,6 @@ use Appwrite\OpenSSL\OpenSSL;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\System\System;
Database::addFilter(
@@ -70,11 +69,11 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
$attributes = $database->find('attributes', [
$attributes = $database->getAuthorization()->skip(fn () => $database->find('attributes', [
Query::equal('collectionInternalId', [$document->getSequence()]),
Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]),
Query::limit($database->getLimitForAttributes()),
]);
]));
foreach ($attributes as $attribute) {
$attributeType = $attribute->getAttribute('type');
@@ -105,12 +104,12 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return $database
return $database->getAuthorization()->skip(fn () => $database
->find('indexes', [
Query::equal('collectionInternalId', [$document->getSequence()]),
Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]),
Query::limit($database->getLimitForIndexes()),
]);
]));
}
);
@@ -120,11 +119,11 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return $database
return $database->getAuthorization()->skip(fn () => $database
->find('platforms', [
Query::equal('projectInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]);
]));
}
);
@@ -134,11 +133,12 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return $database
return $database->getAuthorization()->skip(fn () => $database
->find('keys', [
Query::equal('projectInternalId', [$document->getSequence()]),
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]);
]));
}
);
@@ -148,11 +148,11 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return $database
return $database->getAuthorization()->skip(fn () => $database
->find('devKeys', [
Query::equal('projectInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]);
]));
}
);
@@ -162,11 +162,11 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return $database
return $database->getAuthorization()->skip(fn () => $database
->find('webhooks', [
Query::equal('projectInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]);
]));
}
);
@@ -176,7 +176,7 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn () => $database->find('sessions', [
return $database->getAuthorization()->skip(fn () => $database->find('sessions', [
Query::equal('userInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
@@ -189,7 +189,7 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn () => $database
return $database->getAuthorization()->skip(fn () => $database
->find('tokens', [
Query::equal('userInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
@@ -203,7 +203,7 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn () => $database
return $database->getAuthorization()->skip(fn () => $database
->find('challenges', [
Query::equal('userInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
@@ -217,7 +217,7 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn () => $database
return $database->getAuthorization()->skip(fn () => $database
->find('authenticators', [
Query::equal('userInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
@@ -231,7 +231,7 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn () => $database
return $database->getAuthorization()->skip(fn () => $database
->find('memberships', [
Query::equal('userInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
@@ -251,14 +251,14 @@ Database::addFilter(
default => ['function', 'site']
};
return $database
return $database->getAuthorization()->skip(fn () => $database
->find('variables', [
Query::equal('resourceInternalId', [$document->getSequence()]),
Query::equal('resourceType', $resourceType),
Query::orderAsc('resourceType'),
Query::orderAsc(),
Query::limit(APP_LIMIT_SUBQUERY),
]);
]));
}
);
@@ -294,11 +294,11 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return $database
return $database->getAuthorization()->skip(fn () => $database
->find('variables', [
Query::equal('resourceType', ['project']),
Query::limit(APP_LIMIT_SUBQUERY)
]);
]));
}
);
@@ -331,7 +331,7 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn () => $database
return $database->getAuthorization()->skip(fn () => $database
->find('targets', [
Query::equal('userInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY)
@@ -345,7 +345,7 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
$targetIds = Authorization::skip(fn () => \array_map(
$targetIds = $database->getAuthorization()->skip(fn () => \array_map(
fn ($document) => $document->getAttribute('targetInternalId'),
$database->find('subscribers', [
Query::equal('topicInternalId', [$document->getSequence()]),
@@ -433,3 +433,34 @@ Database::addFilter(
return $value;
}
);
Database::addFilter(
'subQueryOrganizationKeys',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database->getAuthorization()->skip(fn () => $database
->find('keys', [
Query::equal('resourceType', ['teams']),
Query::equal('resourceInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
}
);
Database::addFilter(
'subQueryAccountKeys',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database->getAuthorization()->skip(fn () => $database
->find('keys', [
Query::equal('resourceType', ['users']),
Query::equal('resourceInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
}
);
+360
View File
@@ -0,0 +1,360 @@
<?php
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Account;
use Appwrite\Utopia\Response\Model\AlgoArgon2;
use Appwrite\Utopia\Response\Model\AlgoBcrypt;
use Appwrite\Utopia\Response\Model\AlgoMd5;
use Appwrite\Utopia\Response\Model\AlgoPhpass;
use Appwrite\Utopia\Response\Model\AlgoScrypt;
use Appwrite\Utopia\Response\Model\AlgoScryptModified;
use Appwrite\Utopia\Response\Model\AlgoSha;
use Appwrite\Utopia\Response\Model\Any;
use Appwrite\Utopia\Response\Model\Attribute;
use Appwrite\Utopia\Response\Model\AttributeBoolean;
use Appwrite\Utopia\Response\Model\AttributeDatetime;
use Appwrite\Utopia\Response\Model\AttributeEmail;
use Appwrite\Utopia\Response\Model\AttributeEnum;
use Appwrite\Utopia\Response\Model\AttributeFloat;
use Appwrite\Utopia\Response\Model\AttributeInteger;
use Appwrite\Utopia\Response\Model\AttributeIP;
use Appwrite\Utopia\Response\Model\AttributeLine;
use Appwrite\Utopia\Response\Model\AttributeList;
use Appwrite\Utopia\Response\Model\AttributeLongtext;
use Appwrite\Utopia\Response\Model\AttributeMediumtext;
use Appwrite\Utopia\Response\Model\AttributePoint;
use Appwrite\Utopia\Response\Model\AttributePolygon;
use Appwrite\Utopia\Response\Model\AttributeRelationship;
use Appwrite\Utopia\Response\Model\AttributeString;
use Appwrite\Utopia\Response\Model\AttributeText;
use Appwrite\Utopia\Response\Model\AttributeURL;
use Appwrite\Utopia\Response\Model\AttributeVarchar;
use Appwrite\Utopia\Response\Model\AuthProvider;
use Appwrite\Utopia\Response\Model\BaseList;
use Appwrite\Utopia\Response\Model\Branch;
use Appwrite\Utopia\Response\Model\Bucket;
use Appwrite\Utopia\Response\Model\Collection;
use Appwrite\Utopia\Response\Model\Column;
use Appwrite\Utopia\Response\Model\ColumnBoolean;
use Appwrite\Utopia\Response\Model\ColumnDatetime;
use Appwrite\Utopia\Response\Model\ColumnEmail;
use Appwrite\Utopia\Response\Model\ColumnEnum;
use Appwrite\Utopia\Response\Model\ColumnFloat;
use Appwrite\Utopia\Response\Model\ColumnIndex;
use Appwrite\Utopia\Response\Model\ColumnInteger;
use Appwrite\Utopia\Response\Model\ColumnIP;
use Appwrite\Utopia\Response\Model\ColumnLine;
use Appwrite\Utopia\Response\Model\ColumnList;
use Appwrite\Utopia\Response\Model\ColumnLongtext;
use Appwrite\Utopia\Response\Model\ColumnMediumtext;
use Appwrite\Utopia\Response\Model\ColumnPoint;
use Appwrite\Utopia\Response\Model\ColumnPolygon;
use Appwrite\Utopia\Response\Model\ColumnRelationship;
use Appwrite\Utopia\Response\Model\ColumnString;
use Appwrite\Utopia\Response\Model\ColumnText;
use Appwrite\Utopia\Response\Model\ColumnURL;
use Appwrite\Utopia\Response\Model\ColumnVarchar;
use Appwrite\Utopia\Response\Model\ConsoleVariables;
use Appwrite\Utopia\Response\Model\Continent;
use Appwrite\Utopia\Response\Model\Country;
use Appwrite\Utopia\Response\Model\Currency;
use Appwrite\Utopia\Response\Model\Database;
use Appwrite\Utopia\Response\Model\Deployment;
use Appwrite\Utopia\Response\Model\DetectionFramework;
use Appwrite\Utopia\Response\Model\DetectionRuntime;
use Appwrite\Utopia\Response\Model\DetectionVariable;
use Appwrite\Utopia\Response\Model\DevKey;
use Appwrite\Utopia\Response\Model\Document as ModelDocument;
use Appwrite\Utopia\Response\Model\Error;
use Appwrite\Utopia\Response\Model\ErrorDev;
use Appwrite\Utopia\Response\Model\Execution;
use Appwrite\Utopia\Response\Model\File;
use Appwrite\Utopia\Response\Model\Framework;
use Appwrite\Utopia\Response\Model\FrameworkAdapter;
use Appwrite\Utopia\Response\Model\Func;
use Appwrite\Utopia\Response\Model\Headers;
use Appwrite\Utopia\Response\Model\HealthAntivirus;
use Appwrite\Utopia\Response\Model\HealthCertificate;
use Appwrite\Utopia\Response\Model\HealthQueue;
use Appwrite\Utopia\Response\Model\HealthStatus;
use Appwrite\Utopia\Response\Model\HealthTime;
use Appwrite\Utopia\Response\Model\HealthVersion;
use Appwrite\Utopia\Response\Model\Identity;
use Appwrite\Utopia\Response\Model\Index;
use Appwrite\Utopia\Response\Model\Installation;
use Appwrite\Utopia\Response\Model\JWT;
use Appwrite\Utopia\Response\Model\Key;
use Appwrite\Utopia\Response\Model\Language;
use Appwrite\Utopia\Response\Model\Locale;
use Appwrite\Utopia\Response\Model\LocaleCode;
use Appwrite\Utopia\Response\Model\Log;
use Appwrite\Utopia\Response\Model\Membership;
use Appwrite\Utopia\Response\Model\Message;
use Appwrite\Utopia\Response\Model\Metric;
use Appwrite\Utopia\Response\Model\MetricBreakdown;
use Appwrite\Utopia\Response\Model\MFAChallenge;
use Appwrite\Utopia\Response\Model\MFAFactors;
use Appwrite\Utopia\Response\Model\MFARecoveryCodes;
use Appwrite\Utopia\Response\Model\MFAType;
use Appwrite\Utopia\Response\Model\Migration;
use Appwrite\Utopia\Response\Model\MigrationFirebaseProject;
use Appwrite\Utopia\Response\Model\MigrationReport;
use Appwrite\Utopia\Response\Model\Mock;
use Appwrite\Utopia\Response\Model\MockNumber;
use Appwrite\Utopia\Response\Model\None;
use Appwrite\Utopia\Response\Model\Phone;
use Appwrite\Utopia\Response\Model\Platform;
use Appwrite\Utopia\Response\Model\Preferences;
use Appwrite\Utopia\Response\Model\Project;
use Appwrite\Utopia\Response\Model\Provider;
use Appwrite\Utopia\Response\Model\ProviderRepository;
use Appwrite\Utopia\Response\Model\ProviderRepositoryFramework;
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntime;
use Appwrite\Utopia\Response\Model\ResourceToken;
use Appwrite\Utopia\Response\Model\Row;
use Appwrite\Utopia\Response\Model\Rule;
use Appwrite\Utopia\Response\Model\Runtime;
use Appwrite\Utopia\Response\Model\Session;
use Appwrite\Utopia\Response\Model\Site;
use Appwrite\Utopia\Response\Model\Specification;
use Appwrite\Utopia\Response\Model\Subscriber;
use Appwrite\Utopia\Response\Model\Table;
use Appwrite\Utopia\Response\Model\Target;
use Appwrite\Utopia\Response\Model\Team;
use Appwrite\Utopia\Response\Model\TemplateEmail;
use Appwrite\Utopia\Response\Model\TemplateFramework;
use Appwrite\Utopia\Response\Model\TemplateFunction;
use Appwrite\Utopia\Response\Model\TemplateRuntime;
use Appwrite\Utopia\Response\Model\TemplateSite;
use Appwrite\Utopia\Response\Model\TemplateSMS;
use Appwrite\Utopia\Response\Model\TemplateVariable;
use Appwrite\Utopia\Response\Model\Token;
use Appwrite\Utopia\Response\Model\Topic;
use Appwrite\Utopia\Response\Model\Transaction;
use Appwrite\Utopia\Response\Model\UsageBuckets;
use Appwrite\Utopia\Response\Model\UsageCollection;
use Appwrite\Utopia\Response\Model\UsageDatabase;
use Appwrite\Utopia\Response\Model\UsageDatabases;
use Appwrite\Utopia\Response\Model\UsageFunction;
use Appwrite\Utopia\Response\Model\UsageFunctions;
use Appwrite\Utopia\Response\Model\UsageProject;
use Appwrite\Utopia\Response\Model\UsageSite;
use Appwrite\Utopia\Response\Model\UsageSites;
use Appwrite\Utopia\Response\Model\UsageStorage;
use Appwrite\Utopia\Response\Model\UsageTable;
use Appwrite\Utopia\Response\Model\UsageUsers;
use Appwrite\Utopia\Response\Model\User;
use Appwrite\Utopia\Response\Model\Variable;
use Appwrite\Utopia\Response\Model\VcsContent;
use Appwrite\Utopia\Response\Model\Webhook;
// General
Response::setModel(new None());
Response::setModel(new Any());
Response::setModel(new Error());
Response::setModel(new ErrorDev());
// Lists
Response::setModel(new BaseList('Rows List', Response::MODEL_ROW_LIST, 'rows', Response::MODEL_ROW));
Response::setModel(new BaseList('Documents List', Response::MODEL_DOCUMENT_LIST, 'documents', Response::MODEL_DOCUMENT));
Response::setModel(new BaseList('Tables List', Response::MODEL_TABLE_LIST, 'tables', Response::MODEL_TABLE));
Response::setModel(new BaseList('Collections List', Response::MODEL_COLLECTION_LIST, 'collections', Response::MODEL_COLLECTION));
Response::setModel(new BaseList('Databases List', Response::MODEL_DATABASE_LIST, 'databases', Response::MODEL_DATABASE));
Response::setModel(new BaseList('Indexes List', Response::MODEL_INDEX_LIST, 'indexes', Response::MODEL_INDEX));
Response::setModel(new BaseList('Column Indexes List', Response::MODEL_COLUMN_INDEX_LIST, 'indexes', Response::MODEL_COLUMN_INDEX));
Response::setModel(new BaseList('Users List', Response::MODEL_USER_LIST, 'users', Response::MODEL_USER));
Response::setModel(new BaseList('Sessions List', Response::MODEL_SESSION_LIST, 'sessions', Response::MODEL_SESSION));
Response::setModel(new BaseList('Identities List', Response::MODEL_IDENTITY_LIST, 'identities', Response::MODEL_IDENTITY));
Response::setModel(new BaseList('Logs List', Response::MODEL_LOG_LIST, 'logs', Response::MODEL_LOG));
Response::setModel(new BaseList('Files List', Response::MODEL_FILE_LIST, 'files', Response::MODEL_FILE));
Response::setModel(new BaseList('Buckets List', Response::MODEL_BUCKET_LIST, 'buckets', Response::MODEL_BUCKET));
Response::setModel(new BaseList('Resource Tokens List', Response::MODEL_RESOURCE_TOKEN_LIST, 'tokens', Response::MODEL_RESOURCE_TOKEN));
Response::setModel(new BaseList('Teams List', Response::MODEL_TEAM_LIST, 'teams', Response::MODEL_TEAM));
Response::setModel(new BaseList('Memberships List', Response::MODEL_MEMBERSHIP_LIST, 'memberships', Response::MODEL_MEMBERSHIP));
Response::setModel(new BaseList('Sites List', Response::MODEL_SITE_LIST, 'sites', Response::MODEL_SITE));
Response::setModel(new BaseList('Site Templates List', Response::MODEL_TEMPLATE_SITE_LIST, 'templates', Response::MODEL_TEMPLATE_SITE));
Response::setModel(new BaseList('Functions List', Response::MODEL_FUNCTION_LIST, 'functions', Response::MODEL_FUNCTION));
Response::setModel(new BaseList('Function Templates List', Response::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', Response::MODEL_TEMPLATE_FUNCTION));
Response::setModel(new BaseList('Installations List', Response::MODEL_INSTALLATION_LIST, 'installations', Response::MODEL_INSTALLATION));
Response::setModel(new BaseList('Framework Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, 'frameworkProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK));
Response::setModel(new BaseList('Runtime Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, 'runtimeProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME));
Response::setModel(new BaseList('Branches List', Response::MODEL_BRANCH_LIST, 'branches', Response::MODEL_BRANCH));
Response::setModel(new BaseList('Frameworks List', Response::MODEL_FRAMEWORK_LIST, 'frameworks', Response::MODEL_FRAMEWORK));
Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, 'runtimes', Response::MODEL_RUNTIME));
Response::setModel(new BaseList('Deployments List', Response::MODEL_DEPLOYMENT_LIST, 'deployments', Response::MODEL_DEPLOYMENT));
Response::setModel(new BaseList('Executions List', Response::MODEL_EXECUTION_LIST, 'executions', Response::MODEL_EXECUTION));
Response::setModel(new BaseList('Projects List', Response::MODEL_PROJECT_LIST, 'projects', Response::MODEL_PROJECT, true, false));
Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, false));
Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, true));
Response::setModel(new BaseList('Dev Keys List', Response::MODEL_DEV_KEY_LIST, 'devKeys', Response::MODEL_DEV_KEY, true, false));
Response::setModel(new BaseList('Auth Providers List', Response::MODEL_AUTH_PROVIDER_LIST, 'platforms', Response::MODEL_AUTH_PROVIDER, true, false));
Response::setModel(new BaseList('Platforms List', Response::MODEL_PLATFORM_LIST, 'platforms', Response::MODEL_PLATFORM, true, false));
Response::setModel(new BaseList('Countries List', Response::MODEL_COUNTRY_LIST, 'countries', Response::MODEL_COUNTRY));
Response::setModel(new BaseList('Continents List', Response::MODEL_CONTINENT_LIST, 'continents', Response::MODEL_CONTINENT));
Response::setModel(new BaseList('Languages List', Response::MODEL_LANGUAGE_LIST, 'languages', Response::MODEL_LANGUAGE));
Response::setModel(new BaseList('Currencies List', Response::MODEL_CURRENCY_LIST, 'currencies', Response::MODEL_CURRENCY));
Response::setModel(new BaseList('Phones List', Response::MODEL_PHONE_LIST, 'phones', Response::MODEL_PHONE));
Response::setModel(new BaseList('Metric List', Response::MODEL_METRIC_LIST, 'metrics', Response::MODEL_METRIC, true, false));
Response::setModel(new BaseList('Variables List', Response::MODEL_VARIABLE_LIST, 'variables', Response::MODEL_VARIABLE));
Response::setModel(new BaseList('Status List', Response::MODEL_HEALTH_STATUS_LIST, 'statuses', Response::MODEL_HEALTH_STATUS));
Response::setModel(new BaseList('Rule List', Response::MODEL_PROXY_RULE_LIST, 'rules', Response::MODEL_PROXY_RULE));
Response::setModel(new BaseList('Locale codes list', Response::MODEL_LOCALE_CODE_LIST, 'localeCodes', Response::MODEL_LOCALE_CODE));
Response::setModel(new BaseList('Provider list', Response::MODEL_PROVIDER_LIST, 'providers', Response::MODEL_PROVIDER));
Response::setModel(new BaseList('Message list', Response::MODEL_MESSAGE_LIST, 'messages', Response::MODEL_MESSAGE));
Response::setModel(new BaseList('Topic list', Response::MODEL_TOPIC_LIST, 'topics', Response::MODEL_TOPIC));
Response::setModel(new BaseList('Subscriber list', Response::MODEL_SUBSCRIBER_LIST, 'subscribers', Response::MODEL_SUBSCRIBER));
Response::setModel(new BaseList('Target list', Response::MODEL_TARGET_LIST, 'targets', Response::MODEL_TARGET));
Response::setModel(new BaseList('Transaction List', Response::MODEL_TRANSACTION_LIST, 'transactions', Response::MODEL_TRANSACTION));
Response::setModel(new BaseList('Migrations List', Response::MODEL_MIGRATION_LIST, 'migrations', Response::MODEL_MIGRATION));
Response::setModel(new BaseList('Migrations Firebase Projects List', Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', Response::MODEL_MIGRATION_FIREBASE_PROJECT));
Response::setModel(new BaseList('Specifications List', Response::MODEL_SPECIFICATION_LIST, 'specifications', Response::MODEL_SPECIFICATION));
Response::setModel(new BaseList('VCS Content List', Response::MODEL_VCS_CONTENT_LIST, 'contents', Response::MODEL_VCS_CONTENT));
// Entities
Response::setModel(new Database());
// Collection API Models
Response::setModel(new Collection());
Response::setModel(new Attribute());
Response::setModel(new AttributeList());
Response::setModel(new AttributeString());
Response::setModel(new AttributeInteger());
Response::setModel(new AttributeFloat());
Response::setModel(new AttributeBoolean());
Response::setModel(new AttributeEmail());
Response::setModel(new AttributeEnum());
Response::setModel(new AttributeIP());
Response::setModel(new AttributeURL());
Response::setModel(new AttributeDatetime());
Response::setModel(new AttributeRelationship());
Response::setModel(new AttributePoint());
Response::setModel(new AttributeLine());
Response::setModel(new AttributePolygon());
Response::setModel(new AttributeVarchar());
Response::setModel(new AttributeText());
Response::setModel(new AttributeMediumtext());
Response::setModel(new AttributeLongtext());
// Table API Models
Response::setModel(new Table());
Response::setModel(new Column());
Response::setModel(new ColumnList());
Response::setModel(new ColumnString());
Response::setModel(new ColumnInteger());
Response::setModel(new ColumnFloat());
Response::setModel(new ColumnBoolean());
Response::setModel(new ColumnEmail());
Response::setModel(new ColumnEnum());
Response::setModel(new ColumnIP());
Response::setModel(new ColumnURL());
Response::setModel(new ColumnDatetime());
Response::setModel(new ColumnRelationship());
Response::setModel(new ColumnPoint());
Response::setModel(new ColumnLine());
Response::setModel(new ColumnPolygon());
Response::setModel(new ColumnVarchar());
Response::setModel(new ColumnText());
Response::setModel(new ColumnMediumtext());
Response::setModel(new ColumnLongtext());
Response::setModel(new Index());
Response::setModel(new ColumnIndex());
Response::setModel(new Row());
Response::setModel(new ModelDocument());
Response::setModel(new Log());
Response::setModel(new User());
Response::setModel(new AlgoMd5());
Response::setModel(new AlgoSha());
Response::setModel(new AlgoPhpass());
Response::setModel(new AlgoBcrypt());
Response::setModel(new AlgoScrypt());
Response::setModel(new AlgoScryptModified());
Response::setModel(new AlgoArgon2());
Response::setModel(new Account());
Response::setModel(new Preferences());
Response::setModel(new Session());
Response::setModel(new Identity());
Response::setModel(new Token());
Response::setModel(new JWT());
Response::setModel(new Locale());
Response::setModel(new LocaleCode());
Response::setModel(new File());
Response::setModel(new Bucket());
Response::setModel(new ResourceToken());
Response::setModel(new Team());
Response::setModel(new Membership());
Response::setModel(new Site());
Response::setModel(new TemplateSite());
Response::setModel(new TemplateFramework());
Response::setModel(new Func());
Response::setModel(new TemplateFunction());
Response::setModel(new TemplateRuntime());
Response::setModel(new TemplateVariable());
Response::setModel(new Installation());
Response::setModel(new ProviderRepository());
Response::setModel(new ProviderRepositoryFramework());
Response::setModel(new ProviderRepositoryRuntime());
Response::setModel(new DetectionFramework());
Response::setModel(new DetectionRuntime());
Response::setModel(new DetectionVariable());
Response::setModel(new VcsContent());
Response::setModel(new Branch());
Response::setModel(new Runtime());
Response::setModel(new Framework());
Response::setModel(new FrameworkAdapter());
Response::setModel(new Deployment());
Response::setModel(new Execution());
Response::setModel(new Project());
Response::setModel(new Webhook());
Response::setModel(new Key());
Response::setModel(new DevKey());
Response::setModel(new MockNumber());
Response::setModel(new AuthProvider());
Response::setModel(new Platform());
Response::setModel(new Variable());
Response::setModel(new Country());
Response::setModel(new Continent());
Response::setModel(new Language());
Response::setModel(new Currency());
Response::setModel(new Phone());
Response::setModel(new HealthAntivirus());
Response::setModel(new HealthQueue());
Response::setModel(new HealthStatus());
Response::setModel(new HealthCertificate());
Response::setModel(new HealthTime());
Response::setModel(new HealthVersion());
Response::setModel(new Metric());
Response::setModel(new MetricBreakdown());
Response::setModel(new UsageDatabases());
Response::setModel(new UsageDatabase());
Response::setModel(new UsageTable());
Response::setModel(new UsageCollection());
Response::setModel(new UsageUsers());
Response::setModel(new UsageStorage());
Response::setModel(new UsageBuckets());
Response::setModel(new UsageFunctions());
Response::setModel(new UsageFunction());
Response::setModel(new UsageSites());
Response::setModel(new UsageSite());
Response::setModel(new UsageProject());
Response::setModel(new Headers());
Response::setModel(new Specification());
Response::setModel(new Rule());
Response::setModel(new TemplateSMS());
Response::setModel(new TemplateEmail());
Response::setModel(new ConsoleVariables());
Response::setModel(new MFAChallenge());
Response::setModel(new MFARecoveryCodes());
Response::setModel(new MFAType());
Response::setModel(new MFAFactors());
Response::setModel(new Provider());
Response::setModel(new Message());
Response::setModel(new Topic());
Response::setModel(new Transaction());
Response::setModel(new Subscriber());
Response::setModel(new Target());
Response::setModel(new Migration());
Response::setModel(new MigrationReport());
Response::setModel(new MigrationFirebaseProject());
// Tests (keep last)
Response::setModel(new Mock());
+21 -2
View File
@@ -23,6 +23,8 @@ use Utopia\Logger\Adapter\LogOwl;
use Utopia\Logger\Adapter\Raygun;
use Utopia\Logger\Adapter\Sentry;
use Utopia\Logger\Logger;
use Utopia\Pools\Adapter\Stack as StackPool;
use Utopia\Pools\Adapter\Swoole as SwoolePool;
use Utopia\Pools\Group;
use Utopia\Pools\Pool;
use Utopia\Queue;
@@ -285,7 +287,9 @@ $register->set('pools', function () {
default => throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Invalid scheme'),
};
$pool = new Pool($name, $poolSize, function () use ($type, $resource, $dsn) {
$poolAdapter = System::getEnv('_APP_POOL_ADAPTER', default: 'stack') === 'swoole' ? new SwoolePool() : new StackPool();
$pool = new Pool($poolAdapter, $name, $poolSize, function () use ($type, $resource, $dsn) {
// Get Adapter
switch ($type) {
case 'database':
@@ -309,10 +313,17 @@ $register->set('pools', function () {
default => null
};
case 'cache':
return match ($dsn->getScheme()) {
$adapter = match ($dsn->getScheme()) {
'redis' => new RedisCache($resource()),
default => null
};
if ($adapter !== null) {
$adapter->setMaxRetries(CACHE_RECONNECT_MAX_RETRIES);
$adapter->setRetryDelay(CACHE_RECONNECT_RETRY_DELAY);
}
return $adapter;
default:
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Server error: Missing adapter implementation.");
}
@@ -324,6 +335,12 @@ $register->set('pools', function () {
Config::setParam('pools-' . $key, $config);
}
$reconnectAttempts = (int) System::getEnv('_APP_CONNECTIONS_RECONNECT_ATTEMPTS', 5);
$reconnectSleep = (int) System::getEnv('_APP_CONNECTIONS_RECONNECT_SLEEP', 2);
$group->setReconnectAttempts($reconnectAttempts);
$group->setReconnectSleep($reconnectSleep);
return $group;
});
@@ -360,6 +377,8 @@ $register->set('smtp', function () {
$mail->SMTPSecure = System::getEnv('_APP_SMTP_SECURE', '');
$mail->SMTPAutoTLS = false;
$mail->CharSet = 'UTF-8';
$mail->Timeout = 10; /* Connection timeout */
$mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */
$from = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
$email = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
+494 -164
View File
@@ -4,7 +4,7 @@ use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Databases\TransactionState;
use Appwrite\Event\Audit;
use Appwrite\Event\Audit as AuditEvent;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
use Appwrite\Event\Database as EventDatabase;
@@ -15,19 +15,25 @@ use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\StatsResources;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\GraphQL\Schema;
use Appwrite\Network\Cors;
use Appwrite\Network\Platform;
use Appwrite\Network\Validator\Origin;
use Appwrite\Network\Validator\Redirect;
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\Audit\Adapter\Database as AdapterDatabase;
use Utopia\Audit\Audit;
use Utopia\Auth\Hashes\Argon2;
use Utopia\Auth\Hashes\Sha;
use Utopia\Auth\Proofs\Code;
@@ -43,7 +49,6 @@ use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\DateTime as DatabaseDateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
@@ -64,7 +69,7 @@ use Utopia\Storage\Storage;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Telemetry\Adapter\None as NoTelemetry;
use Utopia\Validator\Hostname;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub as VcsGitHub;
@@ -126,6 +131,9 @@ App::setResource('queueForMails', function (Publisher $publisher) {
App::setResource('queueForBuilds', function (Publisher $publisher) {
return new Build($publisher);
}, ['publisher']);
App::setResource('queueForScreenshots', function (Publisher $publisher) {
return new Screenshot($publisher);
}, ['publisher']);
App::setResource('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
@@ -145,11 +153,14 @@ App::setResource('queueForStatsUsage', function (Publisher $publisher) {
return new StatsUsage($publisher);
}, ['publisher']);
App::setResource('queueForAudits', function (Publisher $publisher) {
return new Audit($publisher);
return new AuditEvent($publisher);
}, ['publisher']);
App::setResource('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
App::setResource('eventProcessor', function () {
return new EventProcessor();
}, []);
App::setResource('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
@@ -159,81 +170,162 @@ App::setResource('queueForMigrations', function (Publisher $publisher) {
App::setResource('queueForStatsResources', function (Publisher $publisher) {
return new StatsResources($publisher);
}, ['publisher']);
App::setResource('platforms', function (Request $request, Document $console, Document $project, Database $dbForPlatform) {
$console->setAttribute('platforms', [ // Always allow current host
'$collection' => ID::custom('platforms'),
'name' => 'Current Host',
'type' => Platform::TYPE_WEB,
'hostname' => $request->getHostname(),
], Document::SET_TYPE_APPEND);
$hostnames = explode(',', System::getEnv('_APP_CONSOLE_HOSTNAMES', ''));
$validator = new Hostname();
foreach ($hostnames as $hostname) {
$hostname = trim($hostname);
if (!$validator->isValid($hostname)) {
continue;
}
$console->setAttribute('platforms', [
'$collection' => ID::custom('platforms'),
'type' => Platform::TYPE_WEB,
'name' => $hostname,
'hostname' => $hostname,
], Document::SET_TYPE_APPEND);
}
/**
* Platform configuration
*/
App::setResource('platform', function () {
return Config::getParam('platform', []);
}, []);
// Add `exp` and `appwrite-callback-{projectId}` schemes
/**
* List of allowed request hostnames for the request.
*/
App::setResource('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) {
$allowed = [...($platform['hostnames'] ?? [])];
/* Add platform configured hostnames */
if (!$project->isEmpty() && $project->getId() !== 'console') {
$project->setAttribute('platforms', [
'$collection' => ID::custom('platforms'),
'type' => Platform::TYPE_SCHEME,
'name' => 'Expo',
'key' => 'exp',
], Document::SET_TYPE_APPEND);
$project->setAttribute('platforms', [
'$collection' => ID::custom('platforms'),
'type' => Platform::TYPE_SCHEME,
'name' => 'Appwrite Callback',
'key' => 'appwrite-callback-' . $project->getId(),
], Document::SET_TYPE_APPEND);
$platforms = $project->getAttribute('platforms', []);
$hostnames = Platform::getHostnames($platforms);
$allowed = [...$allowed, ...$hostnames];
}
$origin = \parse_url($request->getOrigin(), PHP_URL_HOST);
if (empty($origin)) {
$origin = \parse_url($request->getReferer(), PHP_URL_HOST);
/* Add the request hostname if a dev key is found */
if (!$devKey->isEmpty()) {
$allowed[] = $request->getHostname();
}
// Safe if rule with same project ID exists
if (!empty($origin)) {
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin ?? '')));
} else {
$rule = Authorization::skip(
fn () => $dbForPlatform->find('rules', [
Query::equal('domain', [$origin]),
Query::limit(1)
])
)[0] ?? new Document();
$originHostname = parse_url($request->getOrigin(), PHP_URL_HOST);
/* Add request hostname for preflight requests */
if ($request->getMethod() === 'OPTIONS') {
$allowed[] = $originHostname;
}
/* Allow the request origin if a dev key or rule is found */
if ((!$rule->isEmpty() || !$devKey->isEmpty()) && !empty($originHostname)) {
$allowed[] = $originHostname;
}
return array_unique($allowed);
}, ['platform', 'project', 'rule', 'devKey', 'request']);
/**
* List of allowed request schemes for the request.
*/
App::setResource('allowedSchemes', function (Document $project) {
$allowed = [];
if (!$project->isEmpty() && $project->getId() !== 'console') {
/* Add hardcoded schemes */
$allowed[] = 'exp';
$allowed[] = 'appwrite-callback-' . $project->getId();
/* Add platform configured schemes */
$platforms = $project->getAttribute('platforms', []);
$schemes = Platform::getSchemes($platforms);
$allowed = [...$allowed, ...$schemes];
}
return array_unique($allowed);
}, ['project']);
/**
* Rule associated with a request origin.
*/
App::setResource('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) {
$domain = \parse_url($request->getOrigin(), PHP_URL_HOST);
if (empty($domain)) {
return new Document();
}
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$rule = $authorization->skip(function () use ($dbForPlatform, $domain, $isMd5) {
if ($isMd5) {
return $dbForPlatform->getDocument('rules', md5($domain));
}
if (!$rule->isEmpty() && $rule->getAttribute('projectInternalId') === $project->getSequence()) {
$project->setAttribute('platforms', [
'$collection' => ID::custom('platforms'),
'type' => Platform::TYPE_WEB,
'name' => $origin,
'hostname' => $origin,
], Document::SET_TYPE_APPEND);
}
return $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain]),
]) ?? new Document();
});
if ($rule->getAttribute('projectInternalId') !== $project->getSequence()) {
return new Document();
}
return [
...$console->getAttribute('platforms', []),
...$project->getAttribute('platforms', []),
];
}, ['request', 'console', 'project', 'dbForPlatform']);
return $rule;
}, ['request', 'dbForPlatform', 'project', 'authorization']);
App::setResource('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken) {
/**
* CORS service
*/
App::setResource('cors', fn (array $allowedHostnames) => new Cors(
$allowedHostnames,
allowedMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: [
'Accept',
'Origin',
'Cookie',
'Set-Cookie',
// Content
'Content-Type',
'Content-Range',
// Appwrite
'X-Appwrite-Project',
'X-Appwrite-Key',
'X-Appwrite-Dev-Key',
'X-Appwrite-Locale',
'X-Appwrite-Mode',
'X-Appwrite-JWT',
'X-Appwrite-Response-Format',
'X-Appwrite-Timeout',
'X-Appwrite-ID',
'X-Appwrite-Timestamp',
'X-Appwrite-Session',
'X-Appwrite-Platform', // for `$platform` injection and SDK generator
// SDK generator
'X-SDK-Version',
'X-SDK-Name',
'X-SDK-Language',
'X-SDK-Platform',
'X-SDK-GraphQL',
'X-SDK-Profile',
// Caching
'Range',
'Cache-Control',
'Expires',
'Pragma',
// Server to server
'X-Fallback-Cookies',
'X-Requested-With',
'X-Forwarded-For',
'X-Forwarded-User-Agent',
],
allowCredentials: true,
exposedHeaders: [
'X-Appwrite-Session',
'X-Fallback-Cookies',
],
), ['allowedHostnames']);
App::setResource('originValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
if (!$devKey->isEmpty()) {
return new URL();
}
return new Origin($allowedHostnames, $allowedSchemes);
}, ['devKey', 'allowedHostnames', 'allowedSchemes']);
App::setResource('redirectValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
if (!$devKey->isEmpty()) {
return new URL();
}
return new Redirect($allowedHostnames, $allowedSchemes);
}, ['devKey', 'allowedHostnames', 'allowedSchemes']);
App::setResource('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) {
/**
* Handles user authentication and session validation.
*
@@ -251,9 +343,10 @@ App::setResource('user', function (string $mode, Document $project, Document $co
* 5. Regardless of the results from steps 1-4, attempts to fetch the JWT token.
* 6. If the JWT user has a valid session ID, updates the user variable with the user from `projectDB`,
* overwriting the previous value.
* 7. If account API key is passed, use user of the account API key as long as user ID header matches too
*/
Authorization::setDefaultStatus(true);
$authorization->setDefaultStatus(true);
$store->setKey('a_session_' . $project->getId());
@@ -320,19 +413,24 @@ App::setResource('user', function (string $mode, Document $project, Document $co
}
// if (APP_MODE_ADMIN === $mode) {
// if ($user->find('teamInternalId', $project->getAttribute('teamInternalId'), 'memberships')) {
// Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users.
// $authorization->setDefaultStatus(false); // Cancel security segmentation for admin users.
// } else {
// $user = new Document([]);
// }
// }
$authJWT = $request->getHeader('x-appwrite-jwt', '');
if (!empty($authJWT) && !$project->isEmpty()) { // JWT authentication
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_JWT_AND_COOKIE_SET);
}
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
try {
$payload = $jwt->decode($authJWT);
} catch (JWTException $error) {
throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage());
}
$jwtUserId = $payload['userId'] ?? '';
if (!empty($jwtUserId)) {
if ($mode === APP_MODE_ADMIN) {
@@ -348,13 +446,41 @@ App::setResource('user', function (string $mode, Document $project, Document $co
}
}
}
// Account based on account API key
$accountKey = $request->getHeader('x-appwrite-key', '');
$accountKeyUserId = $request->getHeader('x-appwrite-user', '');
if (!empty($accountKeyUserId) && !empty($accountKey)) {
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
}
$accountKeyUser = $dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId));
if (!$accountKeyUser->isEmpty()) {
$key = $accountKeyUser->find(
key: 'secret',
find: $accountKey,
subject: 'keys'
);
if (!empty($key)) {
$expire = $key->getAttribute('expire');
if (!empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
throw new Exception(Exception::ACCOUNT_KEY_EXPIRED);
}
$user = $accountKeyUser;
}
}
}
$dbForProject->setMetadata('user', $user->getId());
$dbForPlatform->setMetadata('user', $user->getId());
return $user;
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken']);
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']);
App::setResource('project', function ($dbForPlatform, $request, $console) {
App::setResource('project', function ($dbForPlatform, $request, $console, $authorization) {
/** @var Appwrite\Utopia\Request $request */
/** @var Utopia\Database\Database $dbForPlatform */
/** @var Utopia\Database\Document $console */
@@ -365,10 +491,10 @@ App::setResource('project', function ($dbForPlatform, $request, $console) {
return $console;
}
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
return $project;
}, ['dbForPlatform', 'request', 'console']);
}, ['dbForPlatform', 'request', 'console', 'authorization']);
App::setResource('session', function (User $user, Store $store, Token $proofForToken) {
if ($user->isEmpty()) {
@@ -391,10 +517,6 @@ App::setResource('session', function (User $user, Store $store, Token $proofForT
return;
}, ['user', 'store', 'proofForToken']);
App::setResource('console', function () {
return new Document(Config::getParam('console'));
}, []);
App::setResource('store', function (): Store {
return new Store();
});
@@ -425,7 +547,15 @@ App::setResource('proofForCode', function (): Code {
return $code;
});
App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project) {
App::setResource('console', function () {
return new Document(Config::getParam('console'));
}, []);
App::setResource('authorization', function () {
return new Authorization();
}, []);
App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, StatsUsage $queueForStatsUsage, Authorization $authorization) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -441,6 +571,8 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId())
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
@@ -461,14 +593,218 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform
->setNamespace('_' . $project->getSequence());
}
/**
* This isolated event handling for `users.*.create` which is based on a `Database::EVENT_DOCUMENT_CREATE` listener may look odd, but it is **intentional**.
*
* Accounts can be created in many ways beyond `createAccount`
* (anonymous, OAuth, phone, etc.), and those flows are probably not covered in event tests; so we handle this here.
*/
$eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) {
// Only trigger events for user creation with the database listener.
if ($document->getCollection() !== 'users') {
return;
}
$queueForEvents
->setEvent('users.[userId].create')
->setParam('userId', $document->getId())
->setPayload($response->output($document, Response::MODEL_USER));
// Trigger functions, webhooks, and realtime events
$queueForFunctions
->from($queueForEvents)
->trigger();
/** Trigger webhooks events only if a project has them enabled */
if (!empty($project->getAttribute('webhooks'))) {
$queueForWebhooks
->from($queueForEvents)
->trigger();
}
/** Trigger realtime events only for non console events */
if ($queueForEvents->getProject()->getId() !== 'console') {
$queueForRealtime
->from($queueForEvents)
->trigger();
}
};
/**
* Purge function events cache when functions are created, updated or deleted.
*/
$functionsEventsCacheListener = function (string $event, Document $document, Document $project, Database $dbForProject) {
if ($document->getCollection() !== 'functions') {
return;
}
if ($project->isEmpty() || $project->getId() === 'console') {
return;
}
$hostname = $dbForProject->getAdapter()->getHostname();
$cacheKey = \sprintf(
'%s-cache-%s:%s:%s:project:%s:functions:events',
$dbForProject->getCacheName(),
$hostname ?? '',
$dbForProject->getNamespace(),
$dbForProject->getTenant(),
$project->getId()
);
$dbForProject->getCache()->purge($cacheKey);
};
$usageDatabaseListener = function (string $event, Document $document, StatsUsage $queueForStatsUsage) {
$value = 1;
switch ($event) {
case Database::EVENT_DOCUMENT_DELETE:
$value = -1;
break;
case Database::EVENT_DOCUMENTS_DELETE:
$value = -1 * $document->getAttribute('modified', 0);
break;
case Database::EVENT_DOCUMENTS_CREATE:
$value = $document->getAttribute('modified', 0);
break;
case Database::EVENT_DOCUMENTS_UPSERT:
$value = $document->getAttribute('created', 0);
break;
}
switch (true) {
case $document->getCollection() === 'teams':
$queueForStatsUsage->addMetric(METRIC_TEAMS, $value); // per project
break;
case $document->getCollection() === 'users':
$queueForStatsUsage->addMetric(METRIC_USERS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage->addReduce($document);
}
break;
case $document->getCollection() === 'sessions': // sessions
$queueForStatsUsage->addMetric(METRIC_SESSIONS, $value); //per project
break;
case $document->getCollection() === 'databases': // databases
$queueForStatsUsage->addMetric(METRIC_DATABASES, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage->addReduce($document);
}
break;
case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$queueForStatsUsage
->addMetric(METRIC_COLLECTIONS, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value);
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage->addReduce($document);
}
break;
case str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_'): //documents
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$collectionInternalId = $parts[3] ?? 0;
$queueForStatsUsage
->addMetric(METRIC_DOCUMENTS, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), $value) // per database
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), $value); // per collection
break;
case $document->getCollection() === 'buckets': //buckets
$queueForStatsUsage
->addMetric(METRIC_BUCKETS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage
->addReduce($document);
}
break;
case str_starts_with($document->getCollection(), 'bucket_'): // files
$parts = explode('_', $document->getCollection());
$bucketInternalId = $parts[1];
$queueForStatsUsage
->addMetric(METRIC_FILES, $value) // per project
->addMetric(METRIC_FILES_STORAGE, $document->getAttribute('sizeOriginal') * $value) // per project
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES), $value) // per bucket
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES_STORAGE), $document->getAttribute('sizeOriginal') * $value); // per bucket
break;
case $document->getCollection() === 'functions':
$queueForStatsUsage
->addMetric(METRIC_FUNCTIONS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage
->addReduce($document);
}
break;
case $document->getCollection() === 'sites':
$queueForStatsUsage
->addMetric(METRIC_SITES, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage
->addReduce($document);
}
break;
case $document->getCollection() === 'deployments':
$queueForStatsUsage
->addMetric(METRIC_DEPLOYMENTS, $value) // per project
->addMetric(METRIC_DEPLOYMENTS_STORAGE, $document->getAttribute('size') * $value) // per project
->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS), $value) // per function
->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value)
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS), $value) // per function
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value);
break;
default:
break;
}
};
// Clone the queues, to prevent events triggered by the database listener
// from overwriting the events that are supposed to be triggered in the shutdown hook.
$queueForEventsClone = new Event($publisher);
$queueForFunctions = new Func($publisherFunctions);
$queueForWebhooks = new Webhook($publisherWebhooks);
$queueForRealtime = new Realtime();
$database
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENTS_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENTS_UPSERT, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENT_CREATE, 'create-trigger-events', fn ($event, $document) => $eventDatabaseListener(
$project,
$document,
$response,
$queueForEventsClone->from($queueForEvents),
$queueForFunctions->from($queueForEvents),
$queueForWebhooks->from($queueForEvents),
$queueForRealtime->from($queueForEvents)
))
->on(Database::EVENT_DOCUMENT_CREATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
->on(Database::EVENT_DOCUMENT_UPDATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
->on(Database::EVENT_DOCUMENT_DELETE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
;
return $database;
}, ['pools', 'dbForPlatform', 'cache', 'project']);
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'queueForStatsUsage', 'authorization']);
App::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
App::setResource('dbForPlatform', function (Group $pools, Cache $cache) {
$adapter = new DatabasePool($pools->get('console'));
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', 'console')
@@ -478,12 +814,12 @@ App::setResource('dbForPlatform', function (Group $pools, Cache $cache) {
$database->setDocumentType('users', User::class);
return $database;
}, ['pools', 'cache']);
}, ['pools', 'cache', 'authorization']);
App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) {
$databases = [];
return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases) {
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -495,13 +831,16 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
$configure = (function (Database $database) use ($project, $dsn) {
$configure = (function (Database $database) use ($project, $dsn, $authorization) {
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId())
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
$database->setDocumentType('users', User::class);
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES)
->setDocumentType('users', User::class)
;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
@@ -531,12 +870,12 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
return $database;
};
}, ['pools', 'dbForPlatform', 'cache']);
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
App::setResource('getLogsDB', function (Group $pools, Cache $cache) {
App::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, &$database) {
return function (?Document $project = null) use ($pools, $cache, $authorization, &$database) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant((int) $project->getSequence());
return $database;
@@ -546,6 +885,8 @@ App::setResource('getLogsDB', function (Group $pools, Cache $cache) {
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
@@ -558,7 +899,12 @@ App::setResource('getLogsDB', function (Group $pools, Cache $cache) {
return $database;
};
}, ['pools', 'cache']);
}, ['pools', 'cache', 'authorization']);
App::setResource('audit', function ($dbForProject) {
$adapter = new AdapterDatabase($dbForProject);
return new Audit($adapter);
}, ['dbForProject']);
App::setResource('telemetry', fn () => new NoTelemetry());
@@ -738,8 +1084,8 @@ App::setResource('passwordsDictionary', function ($register) {
App::setResource('servers', function () {
$platforms = Config::getParam('platforms');
$server = $platforms[APP_PLATFORM_SERVER];
$platforms = Config::getParam('sdks');
$server = $platforms[APP_SDK_PLATFORM_SERVER];
$languages = array_map(function ($language) {
return strtolower($language['name']);
@@ -752,7 +1098,7 @@ App::setResource('promiseAdapter', function ($register) {
return $register->get('promiseAdapter');
}, ['register']);
App::setResource('schema', function ($utopia, $dbForProject) {
App::setResource('schema', function ($utopia, $dbForProject, $authorization) {
$complexity = function (int $complexity, array $args) {
$queries = Query::parseQueries($args['queries'] ?? []);
@@ -762,8 +1108,8 @@ App::setResource('schema', function ($utopia, $dbForProject) {
return $complexity * $limit;
};
$attributes = function (int $limit, int $offset) use ($dbForProject) {
$attrs = Authorization::skip(fn () => $dbForProject->find('attributes', [
$attributes = function (int $limit, int $offset) use ($dbForProject, $authorization) {
$attrs = $authorization->skip(fn () => $dbForProject->find('attributes', [
Query::limit($limit),
Query::offset($offset),
]));
@@ -837,25 +1183,7 @@ App::setResource('schema', function ($utopia, $dbForProject) {
$urls,
$params,
);
}, ['utopia', 'dbForProject']);
App::setResource('contributors', function () {
$path = 'app/config/contributors.json';
$list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : [];
return $list;
});
App::setResource('employees', function () {
$path = 'app/config/employees.json';
$list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : [];
return $list;
});
App::setResource('heroes', function () {
$path = 'app/config/heroes.json';
$list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : [];
return $list;
});
}, ['utopia', 'dbForProject', 'authorization']);
App::setResource('gitHub', function (Cache $cache) {
return new VcsGitHub($cache);
@@ -883,7 +1211,7 @@ App::setResource('smsRates', function () {
return [];
});
App::setResource('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform) {
App::setResource('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) {
$devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', ''));
// Check if given key match project's development keys
@@ -902,7 +1230,7 @@ App::setResource('devKey', function (Request $request, Document $project, array
$accessedAt = $key->getAttribute('accessedAt', 0);
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
$key->setAttribute('accessedAt', DatabaseDateTime::now());
Authorization::skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key));
$authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key));
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
@@ -919,24 +1247,25 @@ App::setResource('devKey', function (Request $request, Document $project, array
/** Update access time as well */
$key->setAttribute('accessedAt', DatabaseDateTime::now());
$key = Authorization::skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key));
$key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key));
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
}
return $key;
}, ['request', 'project', 'servers', 'dbForPlatform']);
App::setResource('team', function (Document $project, Database $dbForPlatform, App $utopia, Request $request) {
return $key;
}, ['request', 'project', 'servers', 'dbForPlatform', 'authorization']);
App::setResource('team', function (Document $project, Database $dbForPlatform, App $utopia, Request $request, Authorization $authorization) {
$teamInternalId = '';
if ($project->getId() !== 'console') {
$teamInternalId = $project->getAttribute('teamInternalId', '');
} else {
$route = $utopia->match($request);
$path = $route->getPath();
$path = !empty($route) ? $route->getPath() : $request->getURI();
if (str_starts_with($path, '/v1/projects/:projectId')) {
$uri = $request->getURI();
$pid = explode('/', $uri)[3];
$p = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $pid));
$p = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $pid));
$teamInternalId = $p->getAttribute('teamInternalId', '');
} elseif ($path === '/v1/projects') {
$teamId = $request->getParam('teamId', '');
@@ -945,7 +1274,7 @@ App::setResource('team', function (Document $project, Database $dbForPlatform, A
return new Document([]);
}
$team = Authorization::skip(fn () => $dbForPlatform->getDocument('teams', $teamId));
$team = $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $teamId));
return $team;
}
}
@@ -954,14 +1283,14 @@ App::setResource('team', function (Document $project, Database $dbForPlatform, A
return new Document([]);
}
$team = Authorization::skip(function () use ($dbForPlatform, $teamInternalId) {
$team = $authorization->skip(function () use ($dbForPlatform, $teamInternalId) {
return $dbForPlatform->findOne('teams', [
Query::equal('$sequence', [$teamInternalId]),
]);
});
return $team;
}, ['project', 'dbForPlatform', 'utopia', 'request']);
}, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization']);
App::setResource(
'isResourceBlocked',
@@ -987,19 +1316,43 @@ App::setResource('previewHostname', function (Request $request, ?Key $apiKey) {
return '';
}, ['request', 'apiKey']);
App::setResource('apiKey', function (Request $request, Document $project): ?Key {
App::setResource('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key {
$key = $request->getHeader('x-appwrite-key');
if (empty($key)) {
return null;
}
return Key::decode($project, $key);
}, ['request', 'project']);
$key = Key::decode($project, $team, $user, $key);
$userHeader = $request->getHeader('x-appwrite-user');
$organizationHeader = $request->getHeader('x-appwrite-organization');
$projectHeader = $request->getHeader('x-appwrite-project');
if (!empty($key->getProjectId())) {
if (empty($projectHeader) || $projectHeader !== $key->getProjectId()) {
throw new Exception(Exception::PROJECT_ID_MISSING);
}
}
if (!empty($key->getUserId())) {
if (empty($userHeader) || $userHeader !== $key->getUserId()) {
throw new Exception(Exception::USER_ID_MISSING);
}
}
if (!empty($key->getTeamId())) {
if (empty($organizationHeader) || $organizationHeader !== $key->getTeamId()) {
throw new Exception(Exception::ORGANIZATION_ID_MISSING);
}
}
return $key;
}, ['request', 'project', 'team', 'user']);
App::setResource('executor', fn () => new Executor());
App::setResource('resourceToken', function ($project, $dbForProject, $request) {
App::setResource('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) {
$tokenJWT = $request->getParam('token');
if (!empty($tokenJWT) && !$project->isEmpty()) { // JWT authentication
@@ -1017,7 +1370,7 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
return new Document([]);
}
$token = Authorization::skip(fn () => $dbForProject->getDocument('resourceTokens', $tokenId));
$token = $authorization->skip(fn () => $dbForProject->getDocument('resourceTokens', $tokenId));
if ($token->isEmpty()) {
return new Document([]);
@@ -1035,7 +1388,7 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
}
return match ($token->getAttribute('resourceType')) {
TOKENS_RESOURCE_TYPE_FILES => (function () use ($token, $dbForProject) {
TOKENS_RESOURCE_TYPE_FILES => (function () use ($token, $dbForProject, $authorization) {
$sequences = explode(':', $token->getAttribute('resourceInternalId'));
$ids = explode(':', $token->getAttribute('resourceId'));
@@ -1046,7 +1399,7 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
$accessedAt = $token->getAttribute('accessedAt', 0);
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) {
$token->setAttribute('accessedAt', DatabaseDateTime::now());
Authorization::skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), $token));
$authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), $token));
}
return new Document([
@@ -1061,39 +1414,16 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
};
}
return new Document([]);
}, ['project', 'dbForProject', 'request']);
}, ['project', 'dbForProject', 'request', 'authorization']);
App::setResource('httpReferrer', function (Request $request): string {
$referrer = $request->getReferer();
return $referrer;
}, ['request']);
App::setResource('transactionState', function (Database $dbForProject, Authorization $authorization) {
return new TransactionState($dbForProject, $authorization);
}, ['dbForProject', 'authorization']);
App::setResource('httpReferrerSafe', function (Request $request, string $httpReferrer, array $platforms, Database $dbForPlatform, Document $project, App $utopia): string {
$origin = \parse_url($request->getOrigin($httpReferrer), PHP_URL_HOST);
$protocol = \parse_url($request->getOrigin($httpReferrer), PHP_URL_SCHEME);
$port = \parse_url($request->getOrigin($httpReferrer), PHP_URL_PORT);
$referrer = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $origin . (!empty($port) ? ':' . $port : '');
// Safe if route is publicly accessible
$route = $utopia->getRoute();
if ($route->getLabel('origin', false)) {
return $referrer;
App::setResource('executionsRetentionCount', function (Document $project, array $plan) {
if ($project->getId() === 'console' || empty($plan)) {
return 0;
}
// Safe if added as web platform
$originValidator = new Origin($platforms);
if ($originValidator->isValid($request->getOrigin($httpReferrer))) {
return $referrer;
}
// Unsafe; Localhost is always safe for ease of local development
$origin = 'localhost';
$protocol = \parse_url($request->getOrigin($httpReferrer), PHP_URL_SCHEME);
$port = \parse_url($request->getOrigin($httpReferrer), PHP_URL_PORT);
$referrer = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $origin . (!empty($port) ? ':' . $port : '');
return $referrer;
}, ['request', 'httpReferrer', 'platforms', 'dbForPlatform', 'project', 'utopia']);
App::setResource('transactionState', function (Database $dbForProject) {
return new TransactionState($dbForProject);
}, ['dbForProject']);
return (int) ($plan['executionsRetentionCount'] ?? 100);
}, ['project', 'plan']);
+139 -35
View File
@@ -29,10 +29,10 @@ use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
@@ -67,6 +67,7 @@ if (!function_exists('getConsoleDB')) {
$adapter = new DatabasePool($pools->get('console'));
$database = new Database($adapter, getCache());
$database
->setDatabase(APP_DATABASE)
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', '_console');
@@ -123,6 +124,7 @@ if (!function_exists('getProjectDB')) {
}
$database
->setDatabase(APP_DATABASE)
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId());
@@ -309,7 +311,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
'value' => '{}'
]);
$statsDocument = Authorization::skip(fn () => $database->createDocument('realtime', $document));
$statsDocument = $database->getAuthorization()->skip(fn () => $database->createDocument('realtime', $document));
break;
} catch (Throwable) {
Console::warning("Collection not ready. Retrying connection ({$attempts})...");
@@ -339,7 +341,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
->setAttribute('timestamp', DateTime::now())
->setAttribute('value', json_encode($payload));
Authorization::skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument));
$database->getAuthorization()->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument));
} catch (Throwable $th) {
$logError($th, "updateWorkerDocument");
}
@@ -370,7 +372,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$payload = [];
$list = Authorization::skip(fn () => $database->find('realtime', [
$list = $database->getAuthorization()->skip(fn () => $database->find('realtime', [
Query::greaterThan('timestamp', DateTime::addSeconds(new \DateTime(), -15)),
]));
@@ -429,10 +431,19 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
]
];
$server->send($realtime->getSubscribers($event), json_encode([
'type' => 'event',
'data' => $event['data']
]));
$subscribers = $realtime->getSubscribers($event); // [connectionId => [subId => queries]]
// For test events, send to all connections with their matched subscription queries
foreach ($subscribers as $connectionId => $matchedSubscriptions) {
$data = $event['data'];
// Send matched subscription IDs
$data['subscriptions'] = array_keys($matchedSubscriptions);
$server->send([$connectionId], json_encode([
'type' => 'event',
'data' => $data
]));
}
}
});
@@ -464,39 +475,67 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
if ($realtime->hasSubscriber($projectId, 'user:' . $userId)) {
$connection = array_key_first(reset($realtime->subscriptions[$projectId]['user:' . $userId]));
$consoleDatabase = getConsoleDB();
$project = Authorization::skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
$project = $consoleDatabase->getAuthorization()->skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
$database = getProjectDB($project);
/** @var Appwrite\Utopia\Database\Documents\User $user */
$user = $database->getDocument('users', $userId);
$roles = $user->getRoles();
$channels = $realtime->connections[$connection]['channels'];
$roles = $user->getRoles($database->getAuthorization());
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
$subscriptionMetadata = $realtime->getSubscriptionMetadata($connection);
$realtime->unsubscribe($connection);
$realtime->subscribe($projectId, $connection, $roles, $channels);
foreach ($subscriptionMetadata as $subscriptionId => $metadata) {
$queries = Query::parseQueries($metadata['queries'] ?? []);
$realtime->subscribe(
$projectId,
$connection,
$subscriptionId,
$roles,
$metadata['channels'] ?? [],
$queries
);
}
// Restore authorization after subscribe
if ($authorization !== null) {
$realtime->connections[$connection]['authorization'] = $authorization;
}
}
}
$receivers = $realtime->getSubscribers($event);
$receivers = $realtime->getSubscribers($event); // [connectionId => [subId => queries]]
if (App::isDevelopment() && !empty($receivers)) {
Console::log("[Debug][Worker {$workerId}] Receivers: " . count($receivers));
Console::log("[Debug][Worker {$workerId}] Receivers Connection IDs: " . json_encode($receivers));
Console::log("[Debug][Worker {$workerId}] Receivers Connection IDs: " . json_encode(array_keys($receivers)));
Console::log("[Debug][Worker {$workerId}] Event Query: " . json_encode(array_values($receivers)));
Console::log("[Debug][Worker {$workerId}] Event: " . $payload);
}
$server->send(
$receivers,
json_encode([
'type' => 'event',
'data' => $event['data']
])
);
$totalMessages = 0;
if (($num = count($receivers)) > 0) {
$register->get('telemetry.messageSentCounter')->add($num);
$stats->incr($event['project'], 'messages', $num);
foreach ($receivers as $connectionId => $matchedSubscriptions) {
$data = $event['data'];
// Send matched subscription IDs
$data['subscriptions'] = array_keys($matchedSubscriptions);
$server->send(
[$connectionId],
json_encode([
'type' => 'event',
'data' => $data
])
);
$totalMessages++;
}
if ($totalMessages > 0) {
$register->get('telemetry.messageSentCounter')->add($totalMessages);
$stats->incr($event['project'], 'messages', $totalMessages);
}
});
} catch (Throwable $th) {
@@ -526,6 +565,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
try {
/** @var Document $project */
$project = $app->getResource('project');
$authorization = $app->getResource('authorization');
/*
* Project Check
@@ -537,13 +577,12 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
if (
array_key_exists('realtime', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['realtime']
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
$timelimit = $app->getResource('timelimit');
$platforms = $app->getResource('platforms');
$user = $app->getResource('user'); /** @var User $user */
/*
@@ -568,13 +607,13 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
* Skip this check for non-web platforms which are not required to send an origin header.
*/
$origin = $request->getOrigin();
$originValidator = new Origin($platforms);
$originValidator = $app->getResource('originValidator');
if (!empty($origin) && !$originValidator->isValid($origin) && $project->getId() !== 'console') {
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
}
$roles = $user->getRoles();
$roles = $user->getRoles($authorization);
$channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId());
@@ -585,14 +624,44 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing channels');
}
$realtime->subscribe($project->getId(), $connection, $roles, $channels);
// Reconstruct subscriptions from query params using helper method
$channelNames = array_keys($channels);
try {
$subscriptionsByIndex = Realtime::constructSubscriptions(
$channelNames,
fn ($channel) => $request->getQuery($channel, null)
);
} catch (QueryException $e) {
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $e->getMessage());
}
// Generate subscription IDs and subscribe
$subscriptionMapping = [];
foreach ($subscriptionsByIndex as $index => $subscription) {
$subscriptionId = ID::unique();
$realtime->subscribe(
$project->getId(),
$connection,
$subscriptionId,
$roles,
$subscription['channels'],
$subscription['queries'] // Query objects
);
$subscriptionMapping[$index] = $subscriptionId;
}
$realtime->connections[$connection]['authorization'] = $authorization;
$user = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT);
$server->send([$connection], json_encode([
'type' => 'connected',
'data' => [
'channels' => array_keys($channels),
'channels' => $channelNames,
'subscriptions' => $subscriptionMapping,
'user' => $user
]
]));
@@ -615,6 +684,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$code = 500;
}
$message = $th->getMessage();
// sanitize 0 && 5xx errors
@@ -644,12 +714,19 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) {
try {
$response = new Response(new SwooleResponse());
$projectId = $realtime->connections[$connection]['projectId'];
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
// Get authorization from connection (stored during onOpen)
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
$database = getConsoleDB();
$database->setAuthorization($authorization);
if ($projectId !== 'console') {
$project = Authorization::skip(fn () => $database->getDocument('projects', $projectId));
$project = $authorization->skip(fn () => $database->getDocument('projects', $projectId));
$database = getProjectDB($project);
$database->setAuthorization($authorization);
} else {
$project = null;
}
@@ -713,9 +790,36 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Session is not valid.');
}
$roles = $user->getRoles();
$channels = Realtime::convertChannels(array_flip($realtime->connections[$connection]['channels']), $user->getId());
$realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels);
$roles = $user->getRoles($database->getAuthorization());
$channelNames = $realtime->connections[$connection]['channels'] ?? [];
$channels = Realtime::convertChannels(array_flip($channelNames), $user->getId());
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
$subscriptionMetadata = $realtime->getSubscriptionMetadata($connection);
$realtime->unsubscribe($connection);
if (!empty($projectId)) {
foreach ($subscriptionMetadata as $subscriptionId => $metadata) {
$queries = Query::parseQueries($metadata['queries'] ?? []);
$realtime->subscribe(
$projectId,
$connection,
$subscriptionId,
$roles,
$metadata['channels'] ?? [],
$queries
);
}
}
// Restore authorization after subscribe
if ($authorization !== null) {
$realtime->connections[$connection]['authorization'] = $authorization;
}
$user = $response->output($user, Response::MODEL_ACCOUNT);
$server->send([$connection], json_encode([
+4 -2
View File
@@ -1,4 +1,5 @@
<?php
use Utopia\Config\Config;
use Utopia\System\System;
$development = $this->getParam('development', false);
@@ -15,7 +16,8 @@ $labelClass = '';
$buttons = [];
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$platform = Config::getParam('platform', []);
$hostname = $platform['consoleHostname'] ?? '';
// TODO: remove this later
if (System::getEnv('_APP_ENV') === 'development') {
$hostname = 'localhost';
@@ -537,4 +539,4 @@ switch ($type) {
</script>
</body>
</html>
</html>
+5 -1
View File
@@ -11,6 +11,7 @@ $httpsPort = $this->getParam('httpsPort', '');
$version = $this->getParam('version', '');
$organization = $this->getParam('organization', '');
$image = $this->getParam('image', '');
$enableAssistant = $this->getParam('enableAssistant', false);
?>services:
traefik:
image: traefik:2.11
@@ -164,6 +165,7 @@ $image = $this->getParam('image', '');
- _APP_MAINTENANCE_RETENTION_SCHEDULES
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
- _APP_GRAPHQL_INTROSPECTION
- _APP_GRAPHQL_MAX_BATCH_SIZE
- _APP_GRAPHQL_MAX_COMPLEXITY
- _APP_GRAPHQL_MAX_DEPTH
@@ -179,7 +181,7 @@ $image = $this->getParam('image', '');
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: <?php echo $organization; ?>/console:7.4.7
image: <?php echo $organization; ?>/console:7.5.7
restart: unless-stopped
networks:
- appwrite
@@ -848,6 +850,7 @@ $image = $this->getParam('image', '');
- _APP_DB_USER
- _APP_DB_PASS
<?php if ($enableAssistant): ?>
appwrite-assistant:
image: appwrite/assistant:0.8.4
container_name: appwrite-assistant
@@ -857,6 +860,7 @@ $image = $this->getParam('image', '');
- appwrite
environment:
- _APP_ASSISTANT_OPENAI_API_KEY
<?php endif; ?>
appwrite-browser:
image: appwrite/browser:0.3.2
+88 -24
View File
@@ -14,6 +14,7 @@ use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Platform\Appwrite;
@@ -21,6 +22,8 @@ use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Swoole\Runtime;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\Audit\Adapter\Database as AdapterDatabase;
use Utopia\Audit\Audit as UtopiaAudit;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
@@ -41,24 +44,42 @@ use Utopia\Queue\Message;
use Utopia\Queue\Publisher;
use Utopia\Queue\Server;
use Utopia\Registry\Registry;
use Utopia\Span\Exporter;
use Utopia\Span\Span;
use Utopia\Span\Storage;
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Telemetry\Adapter\None as NoTelemetry;
Authorization::disable();
Runtime::enableCoroutine();
Span::setStorage(new Storage\Coroutine());
Span::addExporter(new Exporter\Stdout());
global $register;
Server::setResource('register', fn () => $register);
Server::setResource('dbForPlatform', function (Cache $cache, Registry $register) {
Server::setResource('authorization', function () {
$authorization = new Authorization();
$authorization->disable();
return $authorization;
}, []);
Server::setResource('dbForPlatform', function (Cache $cache, Registry $register, Authorization $authorization) {
$pools = $register->get('pools');
$adapter = new DatabasePool($pools->get('console'));
$dbForPlatform = new Database($adapter, $cache);
$dbForPlatform->setNamespace('_console');
$dbForPlatform->setDocumentType('users', User::class);
$dbForPlatform
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setNamespace('_console')
->setDocumentType('users', User::class)
;
return $dbForPlatform;
}, ['cache', 'register']);
}, ['cache', 'register', 'authorization']);
Server::setResource('project', function (Message $message, Database $dbForPlatform) {
$payload = $message->getPayload() ?? [];
@@ -71,7 +92,7 @@ Server::setResource('project', function (Message $message, Database $dbForPlatfo
return $dbForPlatform->getDocument('projects', $project->getId());
}, ['message', 'dbForPlatform']);
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForPlatform) {
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForPlatform, Authorization $authorization) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -103,15 +124,18 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
->setNamespace('_' . $project->getSequence());
}
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
}, ['cache', 'register', 'message', 'project', 'dbForPlatform']);
}, ['cache', 'register', 'message', 'project', 'dbForPlatform', 'authorization']);
Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases): Database {
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -125,7 +149,7 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
if (isset($databases[$dsn->getHost()])) {
$database = $databases[$dsn->getHost()];
$database->setAuthorization($authorization);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
@@ -162,15 +186,18 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
->setNamespace('_' . $project->getSequence());
}
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
};
}, ['pools', 'dbForPlatform', 'cache']);
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
Server::setResource('getLogsDB', function (Group $pools, Cache $cache) {
Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, $database) {
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant((int)$project->getSequence());
return $database;
@@ -180,10 +207,12 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache) {
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
// set tenant
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
@@ -192,7 +221,7 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache) {
return $database;
};
}, ['pools', 'cache']);
}, ['pools', 'cache', 'authorization']);
Server::setResource('abuseRetention', function () {
return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day
@@ -305,6 +334,10 @@ Server::setResource('queueForBuilds', function (Publisher $publisher) {
return new Build($publisher);
}, ['publisher']);
Server::setResource('queueForScreenshots', function (Publisher $publisher) {
return new Screenshot($publisher);
}, ['publisher']);
Server::setResource('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
@@ -411,6 +444,13 @@ Server::setResource('logError', function (Registry $register, Document $project)
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
if ($error->getPrevious() !== null) {
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
$log->addExtra('previousMessage', $error->getPrevious()->getMessage());
}
$log->addExtra('previousFile', $error->getPrevious()->getFile());
$log->addExtra('previousLine', $error->getPrevious()->getLine());
}
foreach (($extras ?? []) as $key => $value) {
$log->addExtra($key, $value);
@@ -431,11 +471,39 @@ Server::setResource('logError', function (Registry $register, Document $project)
Console::warning("Failed: {$error->getMessage()}");
Console::warning($error->getTraceAsString());
if ($error->getPrevious() !== null) {
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
Console::warning("Previous Failed: {$error->getPrevious()->getMessage()}");
}
Console::warning("Previous File: {$error->getPrevious()->getFile()} Line: {$error->getPrevious()->getLine()}");
}
};
}, ['register', 'project']);
Server::setResource('executor', fn () => new Executor());
Server::setResource('getAudit', function (Database $dbForPlatform, callable $getProjectDB) {
return function (Document $project) use ($dbForPlatform, $getProjectDB) {
if ($project->isEmpty() || $project->getId() === 'console') {
$adapter = new AdapterDatabase($dbForPlatform);
return new UtopiaAudit($adapter);
}
$dbForProject = $getProjectDB($project);
$adapter = new AdapterDatabase($dbForProject);
return new UtopiaAudit($adapter);
};
}, ['dbForPlatform', 'getProjectDB']);
Server::setResource('executionsRetentionCount', function (Document $project, array $plan) {
if ($project->getId() === 'console' || empty($plan)) {
return 0;
}
return (int) ($plan['executionsRetentionCount'] ?? 100);
}, ['project', 'plan']);
$pools = $register->get('pools');
$platform = new Appwrite();
$args = $platform->getEnv('argv');
@@ -480,7 +548,8 @@ $worker
->inject('log')
->inject('pools')
->inject('project')
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project) use ($worker, $queueName) {
->inject('authorization')
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project, Authorization $authorization) use ($worker, $queueName) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
if ($logger) {
@@ -496,7 +565,7 @@ $worker
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
$log->addExtra('roles', Authorization::getRoles());
$log->addExtra('roles', $authorization->getRoles());
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
@@ -515,9 +584,4 @@ $worker
Console::error('[Error] Line: ' . $error->getLine());
});
$worker->workerStart()
->action(function () use ($workerName) {
Console::info("Worker $workerName started");
});
$worker->start();
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php doctor $@
exec php /usr/src/code/app/cli.php doctor "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php install $@
exec php /usr/src/code/app/cli.php install "$@"
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
exec php /usr/src/code/app/cli.php interval "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php maintenance $@
exec php /usr/src/code/app/cli.php maintenance "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php migrate $@
exec php /usr/src/code/app/cli.php migrate "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php queue-count --type=failed $@
exec php /usr/src/code/app/cli.php queue-count --type=failed "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php queue-count --type=processing $@
exec php /usr/src/code/app/cli.php queue-count --type=processing "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php queue-count --type=success $@
exec php /usr/src/code/app/cli.php queue-count --type=success "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php queue-retry $@
exec php /usr/src/code/app/cli.php queue-retry "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/realtime.php $@
exec php /usr/src/code/app/realtime.php "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php schedule-executions $@
exec php /usr/src/code/app/cli.php schedule-executions "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php schedule-functions $@
exec php /usr/src/code/app/cli.php schedule-functions "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php schedule-messages $@
exec php /usr/src/code/app/cli.php schedule-messages "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php screenshot $@
exec php /usr/src/code/app/cli.php screenshot "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php sdks $@
exec php /usr/src/code/app/cli.php sdks "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php specs $@
exec php /usr/src/code/app/cli.php specs "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php ssl $@
exec php /usr/src/code/app/cli.php ssl "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php stats-resources $@
exec php /usr/src/code/app/cli.php stats-resources "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
/usr/src/code/vendor/bin/phpunit --configuration /usr/src/code/phpunit.xml $@
exec /usr/src/code/vendor/bin/phpunit --configuration /usr/src/code/phpunit.xml "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php upgrade $@
exec php /usr/src/code/app/cli.php upgrade "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php vars $@
exec php /usr/src/code/app/cli.php vars "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/worker.php audits $@
exec php /usr/src/code/app/worker.php audits "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/worker.php builds $@
exec php /usr/src/code/app/worker.php builds "$@"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/sh
php /usr/src/code/app/worker.php certificates $@
exec php /usr/src/code/app/worker.php certificates "$@"

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