Compare commits

...
Author SHA1 Message Date
loks0n 7e6846f8f8 fix: platform defaults 2025-12-15 15:01:26 +00:00
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
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
loks0n 2cb86c57ef fix: preflight requests 2025-12-12 09:49:04 +00:00
Darshan 6787b0c7f1 Merge branch '1.8.x' into allow-custom-sender 2025-12-12 12:17:45 +05:30
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
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
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
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
Jake BarnbyandGitHub 6320721f51 Merge pull request #10919 from appwrite/bump-lib-1.8.x
Bump migration lib 1.3.9
2025-12-08 09:23:26 +00:00
fogelito 77917c8b46 utopia-php/migration 1.3.9 2025-12-08 10:47:22 +02:00
fogelito d1b47a7916 utopia-php/migration 1.3.8 2025-12-08 09:39:51 +02:00
fogelito 5df786be4b utopia-php/migration (1.3.6 => 1.3.7) 2025-12-08 08:49:28 +02:00
Jake BarnbyandGitHub 65bd9a41d5 Merge pull request #10894 from appwrite/feat-multi-create
Feat multi create
2025-12-08 05:55:52 +00: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
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
Jake BarnbyandGitHub 549570a29e Merge pull request #10895 from appwrite/dat-957
Refactor: use Coroutine context for database and cache functions
2025-12-05 09:59:29 +00:00
Jake Barnby 3c9548bc9d Update specs 2025-12-05 21:47:30 +13:00
Luke B. SilverandGitHub 3459133fd4 Merge pull request #10547 from appwrite/ser-409
fix: Send deployment ready event after updating associated resource
2025-12-05 08:27:47 +00:00
Luke B. SilverandGitHub 1d91ecd9fb Merge pull request #10759 from appwrite/ser-359
fix: Use supported runtimes from env config
2025-12-05 08:27:20 +00:00
Jake Barnby 48038512b9 Fix index validation 2025-12-05 21:05:14 +13:00
Hemachandar b62a1d8b5c fix test 2025-12-05 12:40:35 +05:30
Hemachandar 4ebc67c235 Merge branch '1.8.x' into ser-409 2025-12-05 12:15:08 +05:30
Hemachandar ed80450bd5 fix tanstack 2025-12-05 12:07:47 +05:30
Hemachandar 104368966d Merge branch '1.8.x' into ser-359 2025-12-05 12:01:34 +05:30
Hemachandar 07be361c7c trim space 2025-12-05 11:54:28 +05:30
Jake BarnbyandGitHub bc508353db Merge pull request #10882 from appwrite/release-sdks-4
chore: release sdks for adding getScreenshot method to Avatars service
2025-12-05 04:46:30 +00:00
ArnabChatterjee20k da1285bb22 updated time limit key, cached resource 2025-12-05 09:25:34 +05:30
Chirag Aggarwal b06b33016c fix: format for int32 2025-12-05 08:41:10 +05:30
Aditya OberaiandGitHub 27ede0079f Merge branch '1.7.x' into chore-add-new-site-templates 2025-12-05 02:58:06 +05:30
Torsten DittmannandGitHub a0cf7315fc Merge pull request #10900 from appwrite/fix-use-oauth-classnames-from-config 2025-12-04 20:58:05 +04:00
Torsten Dittmann fad29ee3cd Use configured OAuth2 provider class from config 2025-12-04 20:37:16 +04:00
Jake Barnby 121f94676d Fix test 2025-12-05 01:03:43 +13:00
Jake Barnby df94765cf3 Update validation 2025-12-04 23:25:15 +13:00
Jake Barnby c6ccbfaae1 Remove redundant validation 2025-12-04 21:23:08 +13:00
ArnabChatterjee20k c4cc8595d6 removed redundant static realtime 2025-12-03 22:08:05 +05:30
ArnabChatterjee20k 5878a82b8a add user class 2025-12-03 21:54:45 +05:30
ArnabChatterjee20k 0353b826ac Refactor: use Coroutine context for database and cache functions 2025-12-03 21:32:22 +05:30
Jake Barnby 331e294ef5 Improve min/max validation 2025-12-04 00:03:12 +13:00
Jake Barnby e00dacf9a2 Fix test 2025-12-03 23:24:05 +13:00
Jake Barnby 252a4e12ff Validate format on non-strings 2025-12-03 22:38:37 +13:00
Jake Barnby dce4c6e9f1 Add more tests 2025-12-03 22:15:44 +13:00
Jake Barnby 4de4b475a1 Add more validation cases 2025-12-03 22:07:34 +13:00
Jake Barnby 7d26131019 Validate other relationship options 2025-12-03 21:19:53 +13:00
Jake Barnby 8b4657ff8a Cleanup on create failures 2025-12-03 21:17:38 +13:00
Jake Barnby c09c2848af Merge remote-tracking branch 'origin/1.8.x' into feat-multi-create 2025-12-03 20:49:36 +13:00
Chirag Aggarwal 68ffa5258c fix: roles enum 2025-12-03 12:04:30 +05:30
Chirag Aggarwal 91a1e4cfd0 fix format 2025-12-03 12:02:00 +05:30
Chirag AggarwalandGitHub 7aceae05d5 Merge branch '1.8.x' into release-sdks-4 2025-12-03 12:01:03 +05:30
Chirag Aggarwal 0c114b8d66 fix: enum example 2025-12-03 09:30:43 +05:30
Chirag Aggarwal 8fe1ec48d0 chore: browser permissions enum 2025-12-03 09:28:39 +05:30
Jake BarnbyandGitHub 379b85bb7b Merge pull request #10891 from appwrite/fix-duplicate-document-error
Fix: Duplicate document error while creating file
2025-12-03 01:46:06 +00:00
Damodar LohaniandGitHub 97038c9d05 Merge branch '1.8.x' into fix-duplicate-document-error 2025-12-03 05:17:24 +05:45
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
Jake BarnbyandGitHub 5cc6845a44 Merge pull request #10888 from appwrite/fix-query-call
Fix invalid query
2025-12-02 06:48:28 +00:00
Damodar LohaniandGitHub 0386fde126 Merge pull request #10890 from appwrite/feat-update-geodb
Update GeoDB database
2025-12-02 11:48:20 +05:45
Damodar Lohani 866bd5ebc8 Fix: update test IP 2025-12-02 05:25:25 +00:00
Damodar Lohani d0b1f5f529 Fix: Catch duplicate exception while creating file document 2025-12-02 02:09:29 +00:00
Damodar Lohani 98dbb03155 Feat: update GeoDB database 2025-12-02 01:40:44 +00:00
Jake BarnbyandGitHub 088359c257 Merge pull request #10889 from appwrite/fix-error-setting-user-password
Fix: error setting user password
2025-12-02 01:23:48 +00:00
Damodar Lohani e114d49789 Fix: add test 2025-12-02 01:04:51 +00:00
Damodar Lohani 1df5b71e32 Simplify 2025-12-02 00:59:39 +00:00
Damodar Lohani a166ae5a03 Fix: error setting user password
Fixes Update Password Bug
Fixes #10878
2025-12-02 00:59:19 +00:00
Luke B. SilverandGitHub c5b2b67361 Merge pull request #10887 from appwrite/chore-bump-assistant-0.8.4
chore: bump assistant 0.8.4
2025-12-01 14:40:22 +00:00
Darshan 4c62ecbc95 fix: query call. 2025-12-01 19:59:07 +05:30
loks0n 1a4adc50b2 chore: bump assistant 0.8.4 2025-12-01 14:20:41 +00:00
Chirag Aggarwal aaafdaae52 chore: better examples 2025-12-01 19:21:49 +05:30
Jake Barnby 38af4b83c0 Merge remote-tracking branch 'origin/1.8.x' into feat-multi-create 2025-12-01 21:44:41 +13:00
Jake Barnby cc50ba6b7d Add custom server test 2025-12-01 21:43:50 +13:00
Eldad A. FuxandGitHub adc0d3dbd0 Merge pull request #10884 from appwrite/fix-totp
Fix wrong path
2025-11-29 13:47:21 +01:00
Darshan 27b8b23c07 fix: wrong path. 2025-11-29 17:49:52 +05:30
Chirag Aggarwal 6aa054bc7c chore: release sdks for adding getScreenshot method to Avatars service 2025-11-29 00:56:04 +05:30
Matej BačoandGitHub 64dbb28612 Merge pull request #10880 from appwrite/ser-504
Support query limit and offset in list repos API
2025-11-28 11:49:45 +01:00
Hemachandar 0b5d67a77a update vcs version 2025-11-28 15:43:31 +05:30
Hemachandar e204419b8b handle invalid offset 2025-11-28 15:33:32 +05:30
HemachandarandGitHub 851fb58645 Merge branch '1.8.x' into ser-504 2025-11-28 13:13:06 +05:30
Hemachandar 128478ed00 coderabbit feedback 2025-11-28 12:54:34 +05:30
Hemachandar 6f0c4d6c4e fix limit & offset computation 2025-11-28 12:41:04 +05:30
Matej BačoandGitHub c4366c9de1 Merge pull request #10877 from appwrite/ser-648
Fix file token expiry
2025-11-27 15:54:31 +01:00
Hemachandar d517ca8444 Merge branch '1.8.x' into ser-504 2025-11-27 20:12:58 +05:30
Hemachandar 67ae0b088a update composer.lock 2025-11-27 20:12:22 +05:30
Hemachandar adc5426ce9 feedback 2025-11-27 20:02:39 +05:30
Hemachandar c09f7d5cf9 Fix file token expiry 2025-11-27 19:40:09 +05:30
Jake Barnby 2a88341edc Add attributes + indexes params 2025-11-28 02:33:13 +13:00
Damodar LohaniandGitHub 95abe61bab Merge pull request #10875 from appwrite/fix-wrong-user-type
Add teamId to project array in e2e test
2025-11-27 17:39:33 +05:45
Damodar Lohani 2fdfbf6e61 Fix type 2025-11-27 11:48:32 +00:00
Matej BačoandGitHub c2585e16b1 Merge pull request #10874 from appwrite/revert-10820-ser-550
Revert "Update deploymentId in sites rule for first deployment"
2025-11-27 12:28:55 +01:00
HemachandarandGitHub fd466dfcea Revert "Update deploymentId in sites rule for first deployment" 2025-11-27 16:31:14 +05:30
Damodar LohaniandGitHub 2bd308c236 Merge pull request #10758 from appwrite/feat-appwrite-auth
Feat: utopia auth
2025-11-27 15:11:20 +05:45
Jake BarnbyandGitHub 885a48b598 Merge pull request #10871 from appwrite/feat-custom-realtime-logger
Add realtime logger
2025-11-27 08:51:07 +00:00
Jake Barnby 63652cc155 Add realtime env on config 2025-11-27 17:28:10 +13:00
Damodar Lohani 06c876906c AI suggested fixes 2025-11-27 00:58:32 +00:00
Damodar Lohani ef8d7a45f9 composer update 2025-11-27 00:29:15 +00:00
Jake Barnby 04dd0f807d Add realtime logger 2025-11-27 00:25:04 +13:00
Damodar Lohani 5dd3f9c574 Format 2025-11-26 11:08:13 +00:00
Damodar Lohani cbc8b2ca0d Fix: validator not working 2025-11-26 11:02:00 +00:00
Damodar Lohani f6b04ae63e Fix type 2025-11-26 10:56:56 +00:00
Damodar Lohani 4fd0af509e composer update 2025-11-26 10:53:44 +00:00
Damodar Lohani d539186808 Merge remote-tracking branch 'origin/1.8.x' into feat-appwrite-auth 2025-11-26 10:42:55 +00:00
Hemachandar 1ed3cf0e59 Support query limit and offset in list repos API 2025-11-25 19:24:30 +05:30
Matej BačoandGitHub 11b8742a73 Merge pull request #10869 from appwrite/add-logs
Add logs for migrations sources
2025-11-25 13:09:22 +01:00
Darshan 7e9d74e141 bump: migrations for logs 2025-11-25 17:15:13 +05:30
Matej BačoandGitHub aa98fbb2dc Merge pull request #10855 from appwrite/update-sdk-examples-docs
Update SDK generator and regenerate Java/Kotlin documentation examples
2025-11-25 12:26:46 +01:00
Chirag Aggarwal 8f1b3a8e30 Merge branch '1.8.x' into update-sdk-examples-docs 2025-11-25 14:45:54 +05:30
Matej BačoandGitHub 3d8cee4676 Merge pull request #10820 from appwrite/ser-550
Update deploymentId in sites rule for first deployment
2025-11-25 09:50:16 +01:00
Matej BačoandGitHub 7efbae4310 Merge pull request #10793 from appwrite/feat-SER-448-improve-MFA-docs-endpoint-order
refactor(account): move MFA endpoints to module actions
2025-11-25 09:40:39 +01:00
Jake BarnbyandGitHub 1c9fd39fd6 Merge pull request #10850 from appwrite/browser-bump
Bump: browser version for tests.
2025-11-25 08:28:52 +00:00
Darshan 8c854b7d07 remove: screenshots uploader for comparison. 2025-11-25 13:26:33 +05:30
HemachandarandGitHub c713357131 Merge branch '1.8.x' into ser-550 2025-11-25 13:23:09 +05:30
Darshan fe9ecb6fcb simplify: index. 2025-11-25 13:22:19 +05:30
Darshan ab1541808b Merge remote-tracking branch 'origin/browser-bump' into browser-bump 2025-11-25 12:39:37 +05:30
Darshan 623a8a747d temp: compare the screenshots. 2025-11-25 12:39:31 +05:30
DarshanandGitHub 21237f8e02 Merge branch '1.8.x' into browser-bump 2025-11-25 12:22:51 +05:30
Harsh Mahajan f0ed75f585 update MFA challenge examples to use plural path 2025-11-25 06:08:10 +00:00
Harsh MahajanandGitHub 34ce5099e2 Merge branch '1.8.x' into feat-SER-448-improve-MFA-docs-endpoint-order 2025-11-25 11:30:25 +05:30
Chirag Aggarwal 10171c80da Update SDK generator and regenerate Java/Kotlin documentation examples
Updated SDK generator to fix Java examples Kotlin syntax issues and regenerated all affected documentation examples across multiple SDK versions.
2025-11-25 11:08:11 +05:30
Jake BarnbyandGitHub c6e4afdb49 Merge pull request #10867 from appwrite/feat-jwt-disposition
Custom disposition
2025-11-25 03:25:15 +00:00
Jake Barnby 448e60479d Downgrade migrations 2025-11-25 16:04:46 +13:00
Jake Barnby 8fe7de7ff9 Custom disposition 2025-11-25 15:19:16 +13:00
Matej BačoandGitHub 8e70a5239d Merge pull request #10860 from appwrite/fix-tanstack-nitro-default
Update TanStack root directory
2025-11-24 19:39:49 +01:00
Matej Bačo 28beb27759 Upgrade detection lib 2025-11-24 19:08:09 +01:00
Harsh MahajanGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
b1f5b6946a Update src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Create.php
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-11-24 17:44:25 +05:30
Harsh MahajanGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
9ac594c537 Update src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-11-24 17:44:03 +05:30
Matej Bačo 6547637c96 upgrade lib 2025-11-24 12:58:47 +01:00
Matej Bačo 13a7ead598 Fix TanStack Start default output directory 2025-11-24 12:08:55 +01:00
Harsh MahajanandGitHub d40e793137 Merge branch '1.8.x' into feat-SER-448-improve-MFA-docs-endpoint-order 2025-11-24 14:57:50 +05:30
Damodar Lohani 149fee503f Format 2025-11-23 07:01:11 +00:00
Damodar Lohani 5a1c257a37 composer update 2025-11-23 06:46:15 +00:00
Damodar Lohani 94ba8ee5f2 Merge remote-tracking branch 'origin/1.8.x' into feat-appwrite-auth 2025-11-23 06:43:52 +00:00
Darshan e5fa23ef6c add: 1:1 comparison tests. 2025-11-21 20:05:31 +05:30
DarshanandGitHub 053665eef9 Merge branch '1.8.x' into browser-bump 2025-11-21 13:51:27 +05:30
Jake BarnbyandGitHub 66afd1064d Merge pull request #10853 from appwrite/release-cli-12.0.0
chore: release cli 12.0.0
2025-11-21 08:16:30 +00:00
Harsh Mahajan e2758691af removed singular folder 2025-11-21 06:49:09 +00:00
DarshanandGitHub 9a8a42f479 Merge branch '1.8.x' into browser-bump 2025-11-21 12:10:21 +05:30
Darshan 3347573f17 bump: browser version for tests. 2025-11-21 12:09:33 +05:30
Harsh MahajanandGitHub cceb108fd3 Merge branch '1.8.x' into feat-SER-448-improve-MFA-docs-endpoint-order 2025-11-21 11:59:26 +05:30
Chirag Aggarwal b905686c0e fix typo 2025-11-21 10:28:02 +05:30
Chirag Aggarwal ad43be5dab fix version 2025-11-21 10:26:38 +05:30
Chirag Aggarwal ceb11e039f chore: release cli 12.0.0 2025-11-21 10:25:14 +05:30
Jake BarnbyandGitHub 63e06750cf Merge pull request #10851 from appwrite/chore-index-scope-description
chore: update index descriptions to reflect table terminology
2025-11-21 04:52:38 +00:00
Jake BarnbyandGitHub a4f7cf9f73 Merge pull request #10852 from appwrite/vcs-reference-type
chore: change VCSDeploymentType enum to VCSReferenceType
2025-11-21 04:51:46 +00:00
Chirag Aggarwal ba10bed271 chore: change VCSDeploymentType enum to VCSReferenceType 2025-11-21 09:56:26 +05:30
Steven NguyenandGitHub e2979410da chore: update index descriptions to reflect table terminology
Updated descriptions for indexes to refer to tables instead of collections.
2025-11-20 18:34:45 -08:00
Darshan 9b68b54743 bump: browser version for tests. 2025-11-20 20:18:54 +05:30
Matej BačoandGitHub 0fbdbb386f Merge pull request #10848 from appwrite/template-type-enum-override
fix: override template deployment type enum name
2025-11-20 11:54:40 +01:00
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
Chirag AggarwalandGitHub 50f26e8702 Merge branch '1.8.x' into template-type-enum-override 2025-11-20 16:07:07 +05:30
Levi van Noort 57758296f5 chore: include redis-insights auto-configuration 2025-11-20 11:00:58 +01:00
Chirag Aggarwal a2463f8cc3 fix naming 2025-11-20 15:23:16 +05:30
Levi van Noort a0a1cda2e5 chore: change to include adminir auto-login 2025-11-20 10:51:09 +01:00
Jake BarnbyandGitHub 0cdabbf959 Merge pull request #10847 from appwrite/feat-csv-export
Set downloadUrl on realtime event
2025-11-20 06:01:06 +00:00
Chirag Aggarwal 4b752c15e8 functions too 2025-11-20 10:42:13 +05:30
Chirag Aggarwal fcce6f8d34 fix: override template deployment type enum name 2025-11-20 10:32:56 +05:30
Jake Barnby 2443266ac5 Set downloadUrl on realtime event 2025-11-20 17:10:47 +13:00
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
Matej BačoandGitHub 4616a2e95d Merge pull request #10838 from appwrite/release-flutter-20.3.2
chore: release flutter 20.3.2
2025-11-19 11:42:24 +01:00
Matej BačoandGitHub 9a0e1c1840 Merge pull request #10833 from appwrite/chore-config-for-env
Use config library for dotenv adapter
2025-11-19 11:40:09 +01:00
Matej Bačo bdbe94b56f Improce codeQL 2025-11-19 11:02:29 +01:00
Matej Bačo 0974e1335f Cleanup 2025-11-19 10:38:32 +01:00
Chirag Aggarwal 61f8c99da7 chore: release flutter 20.3.2 2025-11-19 14:11:40 +05:30
Matej Bačo 150d09f656 Update composer.lock 2025-11-19 09:27:53 +01:00
Matej Bačo bfce804f79 Merge branch '1.8.x' into chore-config-for-env 2025-11-19 09:27:42 +01:00
Jake BarnbyandGitHub a479ce63fc Merge pull request #10837 from appwrite/revert-refactor-auth-single-instance
Revert "Merge pull request #10682 from appwrite/refactor-auth-single-…
2025-11-19 04:16:57 +00:00
Jake Barnby 8765d5650a Revert "Merge pull request #10682 from appwrite/refactor-auth-single-instance"
This reverts commit 919b01a1b1, reversing
changes made to deb5039352.
2025-11-19 16:21:06 +13:00
Matej Bačo e3ee724178 Fix during QA 2025-11-18 16:42:57 +01:00
Matej Bačo ce2f2d0ebc Use config library for dotenv adapter 2025-11-18 16:35:02 +01:00
Hemachandar 9f7e3146a4 feedback 2025-11-18 20:08:43 +05:30
Matej BačoandGitHub e6039cf65e Merge pull request #10830 from appwrite/chore-formatinstance
chore: custom format instance in specs
2025-11-18 13:29:27 +01:00
Chirag AggarwalandGitHub 7b3bceeb7d Merge branch '1.8.x' into chore-formatinstance 2025-11-18 15:34:42 +05:30
Jake BarnbyandGitHub 90ef76ad82 Merge pull request #10828 from appwrite/fix-enum-examples
fix: a lot of enum examples generated
2025-11-18 08:10:39 +00:00
Chirag Aggarwal 5a8a900b18 format 2025-11-18 12:50:22 +05:30
Chirag Aggarwal c2645be4e1 chore: custom format instance in specs 2025-11-18 12:47:39 +05:30
Chirag Aggarwal 20221cd6b6 sync composer 2025-11-18 11:43:27 +05:30
Jake BarnbyandGitHub 275ff3dbf2 Merge pull request #10722 from appwrite/feat-per-bucket-image-transformations
feat: per bucket image transformations flag
2025-11-18 06:10:00 +00:00
Chirag Aggarwal b6fbddad81 fix: a lot of enum examples generated 2025-11-18 11:07:50 +05:30
Chirag Aggarwal 39c5b6c4d0 regenerate sdk examples 2025-11-18 09:53:55 +05:30
Chirag Aggarwal f9274dad3f regenerate specs 2025-11-18 09:53:13 +05:30
Chirag Aggarwal 3568af54b3 consistent naming 2025-11-18 09:51:20 +05:30
Chirag Aggarwal 71b6ac0364 Merge branch '1.8.x' into feat-per-bucket-image-transformations 2025-11-18 09:50:02 +05:30
Jake BarnbyandGitHub 919b01a1b1 Merge pull request #10682 from appwrite/refactor-auth-single-instance
Refactor authorization handling across multiple modules to use the ne…
2025-11-18 03:25:48 +00:00
Hemachandar 3d69998f9c another test fix 2025-11-17 18:41:28 +05:30
Hemachandar d2000ee9d8 test fix 2025-11-17 17:58:15 +05:30
Hemachandar 0718f90405 test failures due to merge 2025-11-17 15:51:36 +05:30
Hemachandar 73f5b4227d nit 2025-11-17 14:58:04 +05:30
Hemachandar 52766cfdba Merge branch '1.8.x' into ser-550 2025-11-17 14:57:00 +05:30
Hemachandar ba542554ce functions and more tests 2025-11-17 14:53:26 +05:30
Matej BačoandGitHub deb5039352 Merge pull request #10795 from appwrite/feat-suggested-env-vars
Feat:  Git env variables suggestion
2025-11-14 20:43:26 +01:00
Matej Bačo 793057c823 Update composer.lock 2025-11-14 18:06:20 +01:00
Matej Bačo 4a86b2d5b1 Merge branch '1.8.x' into feat-suggested-env-vars 2025-11-14 18:05:21 +01:00
Hemachandar d36e42377b include VCS 2025-11-14 20:49:06 +05:30
Hemachandar d9522ac6b8 Update deploymentId in sites rule for first deployment 2025-11-14 20:44:48 +05:30
Matej Bačo db1b87ecbb Add env var value detection 2025-11-14 15:49:19 +01:00
Luke B. SilverandGitHub 99d16dfc60 Merge pull request #10819 from appwrite/fix-missing-nullable
fix: missing nullable in datetime
2025-11-14 10:00:06 +00:00
Luke B. SilverandGitHub 725870b1b1 Merge pull request #10817 from appwrite/update-release-sdks
fix: release sdks doc
2025-11-14 09:40:40 +00:00
Chirag Aggarwal cc7b5b7dcf fix: missing nullable in datetime 2025-11-14 15:05:32 +05:30
Chirag Aggarwal 38ef0ad399 fix: release sdks doc 2025-11-14 12:39:37 +05:30
Jake BarnbyandGitHub a1dff370a4 Merge pull request #10815 from appwrite/fix-response-models-multi-methods
fix: generation of all response models in case of multi method endpoints
2025-11-14 06:40:50 +00:00
Jake Barnby c961316f46 Fix params 2025-11-14 19:08:33 +13:00
Jake Barnby 2349401a86 Fix merge 2025-11-14 18:49:26 +13:00
Jake BarnbyandGitHub 6274d5a3e4 Merge branch '1.8.x' into refactor-auth-single-instance 2025-11-14 05:21:40 +00:00
Jake BarnbyandGitHub d0057ec404 Merge pull request #10800 from appwrite/feat-operators
Fix operators in transactions
2025-11-14 05:13:52 +00:00
Jake Barnby ab90563989 Fix tests 2025-11-14 17:31:17 +13:00
Jake BarnbyandGitHub e783f4c234 Merge pull request #10813 from appwrite/feat-csv-export
Feat csv export
2025-11-14 04:16:31 +00:00
Jake Barnby 83b9815644 Merge remote-tracking branch 'origin/1.8.x' into refactor-auth-single-instance 2025-11-14 16:45:57 +13:00
Jake Barnby afb0fcbc6f Update DB 2025-11-14 16:45:21 +13:00
Jake Barnby 082afcec19 Merge branch 'feat-operators' of github.com:appwrite/appwrite into feat-operators 2025-11-14 16:40:24 +13:00
Jake Barnby 2ab36f4cd6 Use helper for tests 2025-11-14 16:40:00 +13:00
Jake Barnby 72c71785a6 Fix doc 2025-11-14 16:35:10 +13:00
Jake BarnbyandGitHub 463eb0e66a Merge pull request #10785 from appwrite/dat-810
Added error message for the backups route
2025-11-14 03:30:35 +00:00
Jake Barnby 0588fb405d Fix keys 2025-11-14 16:16:06 +13:00
Jake Barnby b9211d7141 Add delete old CSV job for maintenance 2025-11-14 15:55:41 +13:00
Jake Barnby a11cf6b002 Merge remote-tracking branch 'origin/1.8.x' into feat-csv-export 2025-11-14 15:34:36 +13:00
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
Luke B. SilverandGitHub a2799b16f8 Merge pull request #10778 from appwrite/fix-nullable-validation
fix: null validation for optional params
2025-11-13 11:39:23 +00:00
Jake BarnbyandGitHub 298e6f9260 Merge branch '1.8.x' into feat-operators 2025-11-13 11:29:05 +00:00
Jake Barnby 3df2efb7ae Fix file reads 2025-11-14 00:15:18 +13:00
Jake BarnbyandGitHub b0a0454c55 Merge pull request #10738 from appwrite/fix-webp-library
Fix webp upload and previews
2025-11-13 11:01:10 +00:00
Jake Barnby b2d228e102 Update specs 2025-11-13 23:42:13 +13:00
Chirag Aggarwal b695ddc7e3 Merge branch '1.8.x' into fix-nullable-validation 2025-11-13 16:06:33 +05:30
Chirag Aggarwal 3bbd87ecb2 fix: openapi too 2025-11-13 16:05:27 +05:30
Chirag Aggarwal fe8c4cb479 fix: generation of all response models in case of multi method endpoints 2025-11-13 15:59:12 +05:30
Matej BačoandGitHub 5e4c74d501 Merge pull request #10486 from appwrite/feat-add-branch-deployments
Add branch-only deployments to Sites without ref tags
2025-11-13 10:33:46 +01:00
shimon 1da4736d5d composer.lock 2025-11-13 11:23:26 +02:00
Jake Barnby c78da61304 Merge remote-tracking branch 'origin/1.8.x' into feat-csv-export 2025-11-13 22:14:10 +13:00
shimon 5722241ff8 Merge branch '1.8.x' of github.com:appwrite/appwrite into refactor-auth-single-instance
# Conflicts:
#	composer.lock
2025-11-13 11:10:40 +02:00
Matej BačoandGitHub fe8593fecc Merge pull request #10751 from appwrite/ser-279
fix: Throw error when file token expiry is in the past
2025-11-13 09:34:46 +01:00
Damodar LohaniandGitHub b07674ccd3 Merge pull request #10812 from appwrite/fix-upgrade-utopia-database
Upgrade utopia-php database
2025-11-13 13:31:29 +05:45
Damodar Lohani aa5e3bf9a9 Merge remote-tracking branch 'origin/1.8.x' into fix-upgrade-utopia-database 2025-11-13 07:00:45 +00:00
Damodar Lohani c1b50e9941 Upgrade utopia-php database 2025-11-13 06:59:03 +00:00
Jake BarnbyandGitHub 3a76249d80 Merge pull request #10811 from appwrite/release-flutter-dart-add-screenshot-examples
release flutter + dart and add screenshot api docs
2025-11-13 06:58:32 +00:00
Chirag Aggarwal b2991709dc update version 2025-11-13 11:38:50 +05:30
Chirag Aggarwal fcc07533e1 update changelog 2025-11-13 11:34:35 +05:30
Chirag Aggarwal 3a6d907b42 release flutter + dart and add screenshot api docs 2025-11-13 11:29:38 +05:30
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
Atharva Deosthale d411b741b3 address comments 2025-11-12 23:06:18 +05:30
Hemachandar bc19e66775 fix tests 2025-11-12 20:27:42 +05:30
Hemachandar 08c9db7c60 use inline validator 2025-11-12 19:23:57 +05:30
Jake Barnby e8f2e78ac4 Use internal bucket 2025-11-12 22:18:25 +13:00
Jake Barnby 37f16ef0bb Fix array conversion 2025-11-12 21:54:37 +13:00
Jake Barnby 46d785bbb8 Fix tests 2025-11-12 21:04:17 +13:00
Damodar LohaniandGitHub a9c9f602ec Merge pull request #10807 from appwrite/feat-update-flutter-default-build-runtime
Update Flutter framework default build runtime version to 3.35
2025-11-12 13:16:20 +05:45
Damodar LohaniandGitHub d90b4c2f19 update framework config 2025-11-12 12:43:34 +05:45
Damodar LohaniandGitHub 79d733486c Update Flutter framework default build runtime version to 3.35 2025-11-12 12:35:31 +05:45
Damodar LohaniandGitHub d862245090 Merge pull request #10806 from appwrite/lohanidamodar-patch-2
Fix: add default fallback to Flutter framework
2025-11-12 12:30:11 +05:45
Damodar LohaniandGitHub aacaadcce0 Fix: add default fallback to Flutter framework 2025-11-12 12:17:15 +05:45
Damodar LohaniandGitHub 69ee6e0659 Merge pull request #10805 from appwrite/lohanidamodar-patch-2
Feat: enable Flutter 3.35 and Dart 3.9 for sites
2025-11-12 10:51:00 +05:45
Jake Barnby e564793089 Merge remote-tracking branch 'origin/1.8.x' into feat-operators
# Conflicts:
#	composer.lock
2025-11-12 17:02:58 +13:00
Jake Barnby 37910a4c5c Fix space 2025-11-12 16:59:50 +13:00
Jake Barnby 0c7551b0c1 Merge remote-tracking branch 'origin/1.8.x' into refactor-auth-single-instance
# Conflicts:
#	app/controllers/api/teams.php
#	composer.lock
2025-11-12 16:51:21 +13:00
Jake Barnby c9582978ed Fix operator parsing on txn ops 2025-11-12 16:48:42 +13:00
Jake Barnby ae21fadf0d Fix event firing on txn ops 2025-11-12 16:48:26 +13:00
Damodar LohaniandGitHub cfd62cf0a1 Update template-runtimes.php 2025-11-12 09:32:12 +05:45
Damodar LohaniandGitHub 279682941a enable dart 2025-11-12 09:31:04 +05:45
Damodar LohaniandGitHub eae26d90af Feat: enable Flutter 3.35 for sites 2025-11-12 09:25:35 +05:45
Jake BarnbyandGitHub b46ebb95a3 Merge pull request #10688 from appwrite/users-add-attributes
Users add new email attributes
2025-11-12 03:19:24 +00:00
Damodar LohaniandGitHub ffcb6a4677 Merge pull request #10804 from appwrite/feat-upgrade-runtimes
Feat: upgrade runtime
2025-11-12 07:26:17 +05:45
Damodar LohaniandGitHub 142e359dba Merge branch '1.8.x' into feat-upgrade-runtimes 2025-11-12 07:16:09 +05:45
Jake BarnbyandGitHub 4130983210 Merge pull request #10407 from appwrite/dat-647
updated migration error attribute size
2025-11-12 01:05:13 +00:00
Damodar Lohani e01ac65ce8 Feat: upgrade runtime 2025-11-12 00:26:27 +00:00
fogelito 97454c0768 EmailValidator chnage to email 2025-11-11 15:33:16 +02:00
fogelito 8b4a56cd9b $emailCanonical 2025-11-11 15:25:10 +02:00
fogelito da0a6b167f EmailValidator 2025-11-11 15:00:09 +02:00
fogelito 27d0c511ac patch users email 2025-11-11 12:09:40 +02:00
fogelito 8a58f7b6d7 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into users-add-attributes 2025-11-11 11:36:01 +02:00
fogelito cc97d78b5a Add try catch 2025-11-11 11:35:18 +02:00
Luke B. SilverandGitHub 91c24b671b Merge pull request #10791 from appwrite/release-cli-php
chore: release cli and php
2025-11-11 09:22:57 +00:00
fogelito b28e536967 Add try catch 2025-11-11 10:35:39 +02:00
Jake Barnby ab5aac00c4 Format 2025-11-11 21:25:21 +13:00
Chirag AggarwalandGitHub 653ac184e1 Merge branch '1.8.x' into feat-per-bucket-image-transformations 2025-11-11 13:53:29 +05:30
Jake Barnby 353e84bb74 Fix keys 2025-11-11 21:10:11 +13:00
Jake Barnby 94614d32d0 Handle deep nested ops 2025-11-11 21:09:12 +13:00
Harsh Mahajan b20c493fa6 changes 2025-11-11 07:50:42 +00:00
Jake Barnby 1efe9fa693 Merge remote-tracking branch 'origin/1.8.x' into feat-operators
# Conflicts:
#	composer.lock
2025-11-11 20:21:15 +13:00
Jake Barnby 74ff9a955b Fix transactions + operators 2025-11-11 20:20:08 +13:00
fogelito ce7f40dd34 Revert response model 2025-11-11 08:16:09 +02:00
fogelito 49318b5dac Merge branch '1.8.x' of https://github.com/appwrite/appwrite into users-add-attributes 2025-11-11 08:09:11 +02:00
Chirag AggarwalandGitHub da736631b2 Merge branch '1.8.x' into release-cli-php 2025-11-11 10:55:52 +05:30
Ebenezer Don 7d568af9cc Update README files to match new tagline and remove no-longer-needed stuff 2025-11-10 21:15:40 +00:00
Harsh MahajanandGitHub b91268b3cd Merge branch '1.8.x' into feat-SER-448-improve-MFA-docs-endpoint-order 2025-11-11 00:13:52 +05:30
Matej BačoandGitHub 9db6f9cb75 Merge pull request #10782 from appwrite/add-elevenlabs-site-template
Add ElevenLabs text-to-speech sites template
2025-11-10 16:35:26 +01:00
fogelito 6096c42ff9 More updates 2025-11-10 17:30:40 +02:00
Matej Bačo 831a261c04 Sync unit tests with DNS lib validator 2025-11-10 16:29:34 +01:00
Matej Bačo c784cf950b Add variables suggestion 2025-11-10 16:20:48 +01:00
Aditya Oberai b06354b99a update tanstack starter and elevenlabs template scores 2025-11-10 15:12:20 +00:00
Aditya OberaiandGitHub a797951bec Merge branch '1.8.x' into add-elevenlabs-site-template 2025-11-10 20:38:21 +05:30
bb0007a1e8 Update app/config/templates/site.php
Co-authored-by: Matej Bačo <matejbacocom@gmail.com>
2025-11-10 20:38:07 +05:30
Aditya Oberai 507717fdf7 generate screenshots from docker command 2025-11-10 14:49:44 +00:00
fogelito 5afae248ae Test Update 2025-11-10 15:00:18 +02:00
fogelito 98255fe628 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into users-add-attributes
# Conflicts:
#	composer.lock
2025-11-10 14:52:25 +02:00
Chirag Aggarwal 057397197b update to 18.0.1 2025-11-10 18:20:31 +05:30
Aditya Oberai c398d5fba9 Add default value for ELEVENLABS_API_KEY env variable 2025-11-10 12:17:29 +00:00
Harsh Mahajan 64b4ea9551 refactor(account): move MFA endpoints to module actions 2025-11-10 11:56:08 +00:00
Damodar Lohani 47b653fd64 Use correct hashing algorithm for oauth2 token 2025-11-10 11:38:47 +00:00
Chirag Aggarwal 4dc700927e fix: examples 2025-11-10 15:39:54 +05:30
Chirag Aggarwal 4a148e6ef9 remove broken execute 2025-11-10 15:06:22 +05:30
Chirag Aggarwal 2ff0ae61c8 update php 2025-11-10 15:00:09 +05:30
Damodar Lohani c8e6fcc090 fix token verify 2025-11-10 09:01:12 +00:00
Damodar Lohani b3df65cf33 remove unused var 2025-11-10 09:01:09 +00:00
Chirag Aggarwal b06460b562 chore: release cli and php 2025-11-10 14:19:26 +05:30
Chirag Aggarwal 74492ded44 update description 2025-11-10 11:35:25 +05:30
Chirag AggarwalandGitHub 9ecffbf4e9 Merge branch '1.8.x' into fix-nullable-validation 2025-11-10 10:19:13 +05:30
Chirag Aggarwal 0ed4b5dbe7 fix imports + add spec changes 2025-11-10 10:16:58 +05:30
Chirag Aggarwal eb17ffd5d2 wrap all optional values with default null with nullable 2025-11-10 10:15:49 +05:30
Jake BarnbyandGitHub 0bf8cead51 Merge pull request #10789 from appwrite/sync-specs
chore: sync specs
2025-11-10 04:33:40 +00:00
Damodar LohaniandGitHub add1142d1f Merge pull request #10786 from appwrite/feat-function-sites-stats
Feat: stats sites and functions runtimes and frameworks
2025-11-10 09:52:36 +05:45
Chirag Aggarwal 86896a6d3c chore: sync specs 2025-11-10 08:51:56 +05:30
Damodar Lohani 4fd4af312d Fix array wrong reference 2025-11-09 08:18:37 +00:00
shimon 3ef6154ebe Update composer.lock to reflect package version upgrades and dependency changes, including Symfony components and Utopia PHP libraries. 2025-11-09 10:10:22 +02:00
Shimon NewmanandGitHub a96ba5499e Merge branch '1.8.x' into refactor-auth-single-instance 2025-11-09 10:06:03 +02:00
shimon 599aa1dcca Merge branch 'main' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-11-09 10:04:37 +02:00
Damodar Lohani 4b7a7496ff Feat: stats sites and functions runtimes and frameworks
- Sites frameworks count
- Functions runtimes count
2025-11-09 07:17:40 +00:00
ArnabChatterjee20k eb0e42b79c added error message for the backups route 2025-11-09 12:13:08 +05:30
Damodar LohaniandGitHub 62e5f54fcb Merge branch '1.8.x' into feat-appwrite-auth 2025-11-09 08:36:27 +05:45
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
Aditya OberaiandGitHub 0c93e7d545 Update app/config/templates/site.php 2025-11-07 23:29:36 +05:30
Aditya Oberai 469d0859fc Add ElevenLabs text-to-speech sites template 2025-11-07 17:07:25 +00:00
ArnabChatterjee20k 7c408d675e pr followups 2025-11-07 14:30:22 +05:30
Luke B. SilverandGitHub 4a96d89ee6 Merge pull request #10765 from appwrite/feat-sdk-release-runtime-config
Set configs for SDK release at runtime
2025-11-07 08:57:43 +00:00
ArnabChatterjee20k 4d47bc2ebf Merge remote-tracking branch 'upstream/1.8.x' into dat-647 2025-11-07 13:48:59 +05:30
ArnabChatterjee20k 119d6c1789 Merge remote-tracking branch 'upstream/1.8.x' into dat-647 2025-11-07 13:19:28 +05:30
Steven NguyenandGitHub 2aa2b8df91 chore: set GITHUB_TOKEN and git email at runtime 2025-11-07 00:06:01 +00:00
Steven 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 NguyenandGitHub 4f1940ac0c Merge pull request #10739 from appwrite/copilot/sub-pr-10738
Add test coverage for webp upload and preview functionality
2025-11-06 15:07:52 -08:00
Steven 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
Steven NguyenandGitHub ef938a1c39 Merge pull request #10732 from appwrite/copilot/fix-google-token-disappears
Fix Google OAuth2 refresh token support
2025-11-06 10:28:02 -08:00
Steven NguyenandGitHub ebee879855 Merge pull request #10748 from appwrite/copilot/update-issue-triage-workflow
Migrate issue-triage workflow from event-based to scheduled batch processing with duplicate detection
2025-11-06 08:14:25 -08:00
shimon ae2ffd259c Remove debug output from HealthCustomServerTest 2025-11-06 17:54:44 +02:00
shimon 3d471bae56 health db debug 2025-11-06 17:37:31 +02:00
shimon d0f2293755 debug health db 2025-11-06 16:42:58 +02:00
shimon d6bd26da43 Merge branch 'refactor-auth-single-instance' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-11-06 16:12:02 +02:00
shimon 16d6c9e5e3 debug dbHelth 2025-11-06 16:11:57 +02:00
Matej BačoandGitHub e342009c9e Merge pull request #10771 from appwrite/Update-facts-on-Github-sites-and-functions
feat: Add new tips to Comment class
2025-11-06 13:50:27 +01:00
Matej BačoandGitHub 41d89f1b7a Merge pull request #10773 from appwrite/chore-remove-locale-cache
Chore: remove locale cache
2025-11-06 13:49:32 +01:00
Matej Bačo ad6aff6af2 remove locale cache 2025-11-06 13:24:17 +01:00
Harsh MahajanandGitHub 9b52f6f8f6 Add new tips to Comment class 2025-11-06 14:17:31 +05:30
Matej BačoandGitHub ad46d057d5 Merge pull request #10593 from appwrite/feat-SER-193-Update-facts-on-github-sites-and-functions
Feat: update facts on GitHub sites and functions
2025-11-06 09:35:53 +01:00
Damodar Lohani 9ef6fa9b4c Merge remote-tracking branch 'origin/1.8.x' into feat-appwrite-auth 2025-11-06 05:58:51 +00:00
Damodar Lohani 9fe74bdd22 update database library 2025-11-06 05:57:36 +00:00
Damodar Lohani fa2b46c7e7 Fix: dev dependency 2025-11-06 02:26:47 +00:00
Damodar Lohani 13f9900802 fix version 2025-11-06 02:12:00 +00:00
Damodar Lohani 3c93004cde Fix: switch database version 2025-11-06 02:05:18 +00:00
Damodar Lohani 631fd2ea4c Merge remote-tracking branch 'origin/1.8.x' into feat-appwrite-auth 2025-11-06 02:04:38 +00:00
copilot-swe-agent[bot]andstnguyen90 8a9d0a3f33 Rename logo-after.webp to image.webp
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-11-05 19:30:21 +00:00
fogelito 1f0e3e0f51 lock file 2025-11-05 13:21:52 +02:00
shimon 26d3cef528 Merge branch '1.8.x' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-11-05 11:33:07 +02:00
shimon fb7691be3b Merge branch 'main' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-11-05 11:27:25 +02:00
Damodar Lohani eb7be569eb format 2025-11-05 07:10:47 +00:00
Damodar Lohani 88cbc36a02 remove Appwrite\Auth\Auth 2025-11-05 07:09:18 +00:00
Damodar Lohani 6a60b7d25b format 2025-11-05 05:33:22 +00:00
Damodar Lohani 343d443903 Fix realtime 2025-11-05 05:31:17 +00:00
Damodar Lohani 12c27edaf9 Fix user test 2025-11-05 05:23:32 +00:00
Damodar Lohani 0f9b13e251 use argon default 2025-11-05 05:09:36 +00:00
Damodar Lohani 965c4738e7 fix user's attributes 2025-11-05 05:06:40 +00:00
Damodar Lohani 4679eff4b8 use argon 2 default 2025-11-05 03:34:52 +00:00
Damodar Lohani 3cfb5d695f Fix list identities 2025-11-05 03:33:02 +00:00
Jake BarnbyandGitHub 501cba216b Update app/init/resources.php 2025-11-05 16:09:20 +13:00
Damodar Lohani 7ef5a7edaf Fix teams 2025-11-05 02:39:03 +00:00
Damodar Lohani 8adeb40c1d fix team get membership 2025-11-05 02:33:22 +00:00
Damodar Lohani ae105165ab fix team logs endpoint 2025-11-05 02:28:56 +00:00
Damodar Lohani 5dfee10369 Fix logs endpoint 2025-11-05 02:16:37 +00:00
Damodar Lohani 44b5ed90d5 composer update 2025-11-05 02:06:14 +00:00
Damodar Lohani e70d5c46b6 Merge branch 'feat-appwrite-auth' of https://github.com/appwrite/appwrite into feat-appwrite-auth 2025-11-05 02:05:25 +00:00
Damodar Lohani c9d66c52b4 composer update 2025-11-05 02:04:39 +00:00
Damodar Lohani 4d22f4252f Merge remote-tracking branch 'origin/1.8.x' into feat-appwrite-auth 2025-11-05 01:59:42 +00:00
Damodar Lohani 9130a7102e Fix use correct user type 2025-11-05 01:41:15 +00:00
Damodar Lohani 2a7d89ea7b Fix missing auth vars 2025-11-05 01:14:36 +00:00
Steven NguyenandGitHub c910c98b32 chore: install ssh and gh commands for dev
This will allow creating github releases for SDKs
2025-11-04 16:18:49 +00:00
fogelito fe9d49c346 Response Model 2025-11-04 17:13:53 +02:00
shimon f3fd2fd191 composer.lock 2025-11-04 17:11:45 +02:00
Shimon NewmanandGitHub a970969c4d Merge branch '1.8.x' into refactor-auth-single-instance 2025-11-04 17:06:53 +02:00
Matej BačoandGitHub 6c1e4434a6 Merge pull request #10763 from appwrite/update-readme-file
Update README.md
2025-11-04 15:52:25 +01:00
shimon f0e4c9184d Update composer.json and composer.lock to reflect new version constraints for utopia-php/audit and utopia-php/database, ensuring compatibility with the latest releases. Adjusted versioning for utopia-php/migration and appwrite/sdk-generator to their respective latest versions. 2025-11-04 16:44:09 +02:00
VeereshandGitHub ba6e389f81 Update README.md 2025-11-04 19:33:30 +05:30
Chirag AggarwalandGitHub decad9ff9d Merge branch '1.8.x' into feat-per-bucket-image-transformations 2025-11-04 14:31:04 +05:30
Damodar LohaniandGitHub e264ef53f4 Merge branch '1.8.x' into feat-appwrite-auth 2025-11-04 14:08:15 +05:45
Damodar Lohani 31ffc9ea92 format 2025-11-04 08:12:46 +00:00
Damodar Lohani 81480f4492 remove deprecated methods 2025-11-04 07:53:14 +00:00
Damodar Lohani 4b15fec1d7 Remove further auth static methods 2025-11-04 07:51:03 +00:00
Damodar Lohani 918ad4a545 remove auth test and add user test 2025-11-04 07:41:42 +00:00
Damodar Lohani dff7aa6ce0 format 2025-11-04 07:12:42 +00:00
Damodar Lohani 5ef717d0fd More replacement for auth checks 2025-11-04 07:12:34 +00:00
Hemachandar 3739ee26d8 fix: Use supported runtimes from env config 2025-11-04 12:09:42 +05:30
copilot-swe-agent[bot]andstnguyen90 2d332264c6 Replace with simple webp image for testing
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-11-04 06:16:41 +00:00
Damodar Lohani 437d914e90 More refactor and fixes 2025-11-04 06:08:35 +00:00
Damodar Lohani 897f512b47 Refactor remove moved to utopia auth 2025-11-04 05:37:45 +00:00
Damodar Lohani df1f14c853 Refactor realtime 2025-11-04 05:25:20 +00:00
Damodar Lohani 68cce71527 refactor realtime 2025-11-04 05:22:18 +00:00
Damodar Lohani 2ae474971d use new session verify 2025-11-04 03:48:57 +00:00
Damodar Lohani 617c1aaee3 update db with custom types support 2025-11-04 02:34:07 +00:00
copilot-swe-agent[bot]andstnguyen90 2f843153da Add duplicate/related issues detection and listing to triage workflow
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-11-03 20:02:03 +00:00
copilot-swe-agent[bot]andstnguyen90 df8fb23e75 Fix test assertion for file count with offset
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-11-03 18:13:21 +00:00
Hemachandar 60e57eb9c5 lint 2025-11-03 17:29:48 +05:30
Hemachandar 10a9845898 fix: Throw error when file token expiry is in the past 2025-11-03 16:55:13 +05:30
shimon dc43b5977f Inject authorization dependency into the Update class to enhance authorization handling for string column updates. 2025-11-03 12:58:22 +02:00
shimon e1fbaf14eb Inject authorization dependency into the Update class for improved authorization handling in relationship updates. 2025-11-03 12:51:21 +02:00
shimon d721425cfe composer.lock 2025-11-03 12:33:07 +02:00
shimon 02dd275be4 Update composer.lock with new reference for utopia-php/database and modify Upsert action method to remove unused Authorization parameter and enhance document validation. This improves code clarity and maintainability. 2025-11-03 11:54:50 +02:00
Damodar Lohani 50cd6a737d Update resources 2025-11-03 09:39:52 +00:00
Damodar Lohani a575c07468 Fix user resource 2025-11-03 09:32:56 +00:00
Damodar Lohani 071ec671e0 Feat: utopia auth 2025-11-03 09:13:08 +00:00
shimon 3391563829 Remove unused Authorization import from Bulk Delete.php to clean up code and improve maintainability. 2025-11-03 11:07:17 +02:00
shimon 5709265f6c linter 2025-11-03 11:02:30 +02:00
shimon 89d5a28c28 Refactor authorization handling to consistently utilize the Authorization class across multiple controllers and functions. Update method signatures to include Authorization as a parameter, ensuring cleaner and more maintainable code. This change enhances the overall structure and readability of the authorization logic. 2025-11-03 10:37:05 +02:00
shimon f613a06d19 composer.lock 2025-11-03 10:04:01 +02:00
Shimon NewmanandGitHub 30f4b846d2 Merge branch '1.8.x' into refactor-auth-single-instance 2025-11-03 09:21:13 +02:00
shimon a3742bc65e compose.lock 2025-11-03 09:13:04 +02:00
shimon cd1b2b6c05 Refactor authorization handling by replacing method calls with direct class references, ensuring consistent usage of the Authorization class across various modules. Update method signatures to include Authorization as a parameter where necessary, and remove unnecessary injections. Additionally, correct the documentation in CHANGES.md for clarity. 2025-11-03 09:07:44 +02:00
shimon c13aed651a Refactor storage file retrieval to use queries directly for counting, improving consistency in authorization handling. Update Action class to remove unnecessary authorization skip, and include authorization in Email/Create class constructor for better dependency management. 2025-11-02 21:24:11 +02:00
shimon dbccb16784 linter 2025-11-02 19:01:59 +02:00
shimon c0be333c0f Merge branch 'refactor-auth-single-instance' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-11-02 18:58:51 +02:00
shimon d96c7eb3e9 Enhance TransactionState to utilize Authorization class for improved transaction management. Updated method signatures and resource initialization to include authorization, ensuring consistent handling across the application. 2025-11-02 18:58:41 +02:00
Shimon NewmanandGitHub 4fca374fe9 Merge branch '1.8.x' into refactor-auth-single-instance 2025-11-02 18:15:26 +02:00
shimon 9b82dd0411 Update composer.lock to reflect version upgrades for utopia-php/detector (0.2.1 to 0.2.2) and appwrite/sdk-generator (1.4.16 to 1.5.0), including updated references and source URLs. 2025-11-02 18:14:31 +02:00
shimon 0904881de8 Refactor authorization handling to ensure consistent usage of the Authorization class across various modules, enhancing code maintainability and clarity. Update method signatures to include authorization as a parameter where necessary. 2025-11-02 18:11:56 +02:00
fogelito 6a41f8b2b9 Remove todos 2025-11-02 11:16:12 +02:00
fogelito 503145734b Move user Document out of try block 2025-11-02 11:06:56 +02:00
fogelito 371cab0658 try catch EmailCanonical 2025-11-02 10:59:28 +02:00
fogelito ca8cb6f260 Add utopia-php/emails 2025-11-02 09:13:55 +02:00
copilot-swe-agent[bot]andstnguyen90 1f9afc8cab Update issue-triage workflow to run daily and process last 24h issues
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-11-01 15:47:43 +00:00
copilot-swe-agent[bot] c5d4551cab Initial plan 2025-11-01 15:39:50 +00:00
copilot-swe-agent[bot]andstnguyen90 d1511d5ba1 Refactor webp tests per review feedback
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-10-30 22:26:30 +00:00
copilot-swe-agent[bot]andstnguyen90 4a8651ab39 Add webp test cases for upload/view and preview
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-10-30 21:58:02 +00:00
copilot-swe-agent[bot]andstnguyen90 de4f473da1 Initial plan for adding webp test cases
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-10-30 21:56:12 +00:00
copilot-swe-agent[bot] ce539972a5 Initial plan 2025-10-30 21:51:50 +00:00
Steven NguyenandGitHub 458100cd69 Upgrade base image to version 0.10.5
Ensure libwebp is installed in the base image when imagemagick is installed so that imagemagick works with webp.
2025-10-30 14:42:18 -07:00
fogelito fea1630909 Remove redundant emailVerification 2025-10-30 18:18:09 +02:00
fogelito e40e88c322 Update emails attributes 2025-10-30 18:09:36 +02:00
copilot-swe-agent[bot]andstnguyen90 627adb43e0 Add access_type=offline and prompt=consent to Google OAuth2 login URL to enable refresh tokens
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2025-10-29 23:12:28 +00:00
copilot-swe-agent[bot] 9ad08694ed Initial plan 2025-10-29 23:01:33 +00:00
Shimon NewmanandGitHub d38399dcc0 Merge branch '1.8.x' into refactor-auth-single-instance 2025-10-29 20:38:04 +02:00
shimon 68b0fe8c70 linter 2025-10-29 20:33:36 +02:00
shimon 7c2a1ade1f Merge branch 'refactor-auth-single-instance' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-10-29 20:22:00 +02:00
shimon 5c2828bc78 Refactor authorization handling across multiple modules to use a single instance of the Authorization class. 2025-10-29 20:21:41 +02:00
111b2abeeb Update app/cli.php
Co-authored-by: Jake Barnby <jakeb994@gmail.com>
2025-10-29 20:19:49 +02:00
Shimon NewmanGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
9503507011 Update src/Appwrite/Utopia/Response.php
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-10-29 11:20:49 +02:00
shimon a6d1f46133 addressing comments 2025-10-29 10:38:09 +02:00
shimon 145b884628 compose.lock 2025-10-28 15:07:26 +02:00
shimon 0e22b06f0a linter 2025-10-28 15:01:08 +02:00
shimon 9e79d883a0 Refactor authorization retrieval in realtime and update imports in migration files; clean up whitespace in tests 2025-10-28 14:16:26 +02:00
Chirag AggarwalandGitHub 02e8a7b429 Merge branch '1.8.x' into feat-per-bucket-image-transformations 2025-10-28 15:12:35 +05:30
Chirag Aggarwal ca861119f9 keep in same 2025-10-28 15:10:51 +05:30
Chirag Aggarwal 0bfbae4cdd add to both dbs 2025-10-28 15:09:11 +05:30
shimon 61de3b9e08 Merge branch 'refactor-auth-single-instance' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-10-28 11:38:08 +02:00
Chirag Aggarwal 7b1e163c77 move to check 2025-10-28 15:07:24 +05:30
Chirag Aggarwal 3f710ddc70 fix: test 2025-10-28 15:06:43 +05:30
Chirag Aggarwal 1546add5d5 add to both dbs 2025-10-28 15:05:14 +05:30
Shimon NewmanandGitHub 734c44c1da Merge branch '1.8.x' into refactor-auth-single-instance 2025-10-28 11:30:43 +02:00
shimon 68524067cf migrations tests fix 2025-10-28 11:29:14 +02:00
shimon cc6319a2c7 singletone fix 2025-10-28 11:09:20 +02:00
Chirag Aggarwal 4aaaa460b2 feat: per bucket image transformations flag 2025-10-28 14:13:38 +05:30
Shimon NewmanandGitHub dee3d375db Merge branch '1.8.x' into refactor-auth-single-instance 2025-10-28 10:01:33 +02:00
shimon 462634d311 Update dependencies in composer.json and composer.lock; upgrade utopia-php/detector to version 0.2.*, and utopia-php/migration to version 1.3.3. Modify exception messages in storage API for better clarity on authorization failures. Adjust test assertions to reflect updated error messages. 2025-10-28 09:09:54 +02:00
shimon e769cf4e48 Remove vendor path for utopia-php/database in docker-compose.yml 2025-10-27 21:23:03 +02:00
shimon fbaffa980a linter 2025-10-27 20:46:58 +02:00
shimon 26f61c1855 add back registers.php 2025-10-27 19:34:49 +02:00
shimon dae62d863a Merge remote-tracking branch 'origin/refactor-auth-single-instance' into refactor-auth-single-instance 2025-10-27 19:27:33 +02:00
shimon 681fa198c9 composer.lock 2025-10-27 19:27:16 +02:00
shimon e7b8e6c90d Merge branch '1.8.x' of github.com:appwrite/appwrite into refactor-auth-single-instance
# Conflicts:
#	composer.json
#	composer.lock
2025-10-27 19:26:53 +02:00
shimon d7cc3895cb Update composer.lock to reflect version changes for utopia-php/detector, remove unused registers.php file, and clean up whitespace in cli.php and worker.php. Refactor database resource initialization in resources.php for consistency. 2025-10-27 19:23:55 +02:00
Shimon NewmanandGitHub a0b21a0bd4 Merge branch '1.8.x' into refactor-auth-single-instance 2025-10-27 16:24:08 +02:00
shimon 65e99ff0de Merge remote-tracking branch 'origin/refactor-auth-single-instance' into refactor-auth-single-instance 2025-10-27 16:20:28 +02:00
shimon ecd6880fe0 composer.lock 2025-10-27 16:20:07 +02:00
shimon e20adbba61 Merge branch 'main' of github.com:appwrite/appwrite into refactor-auth-single-instance 2025-10-27 16:18:50 +02:00
shimon 30d58e6371 Update composer dependencies for utopia-php/audit and utopia-php/database, reflecting new versioning and aliases. Refactor document validation in Create operation to utilize the new authorization method for better security handling. 2025-10-27 16:14:04 +02:00
fogelito 037d60633b Add emailCanonical 2025-10-27 15:59:20 +02:00
fogelito 218bb4c537 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into users-add-attributes 2025-10-27 13:37:33 +02:00
shimon 6ed2000478 Update composer.lock and refactor authorization handling in CLI and worker modules. Adjust Docker configuration to include database source path. Modify error handling in general controller and enhance permission setting in document creation. Fix privileged user role retrieval in function execution. Ensure consistent authorization usage across various resources. 2025-10-27 12:09:23 +02:00
Harsh MahajanandGitHub 1468231de7 Update Comment.php 2025-10-23 20:20:16 +05:30
fogelito c13a70e226 Add users attributes 2025-10-23 12:38:18 +03:00
shimon 25403ea35e composer.lock 2025-10-22 16:43:49 +03:00
shimon 815c75fa9a Merge branch '1.8.x' of github.com:appwrite/appwrite into refactor-auth-single-instance
# Conflicts:
#	composer.lock
2025-10-22 16:42:11 +03:00
shimon 8b3b07a710 linter 2025-10-22 16:40:00 +03:00
shimon 33f88fa5e0 Refactor authorization handling across multiple modules to use the new getAuthorization() method. Update CHANGES.md to reflect the change in usage worker authorization. Adjust composer.json to specify the correct version for utopia-php/database. Update plugin-api-version in composer.lock. 2025-10-22 16:35:48 +03:00
Harsh MahajanandGitHub 32dae9ad7c Update Comment.php 2025-10-22 10:05:18 +05:30
Harsh MahajanandGitHub be5c5f9420 Merge branch 'main' into feat-SER-193-Update-facts-on-github-sites-and-functions 2025-10-22 09:53:45 +05:30
Harsh MahajanGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
64c3b7a2ff Update src/Appwrite/Vcs/Comment.php
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-10-02 23:19:34 +05:30
Harsh Mahajan 61a8f589ac port issue 2025-10-02 17:44:21 +00:00
Harsh Mahajan 92ccb6e4ea feat(vcs): expand GitHub comment tips 2025-10-02 17:43:30 +00:00
Khushboo VermaandGitHub 8b663f041a Merge branch '1.8.x' into ser-409 2025-09-25 17:02:30 +05:30
Hemachandar 52d656194c fix: Send deployment ready event after updating associated resource 2025-09-24 18:49:14 +05:30
Atharva Deosthale 5963b2baf7 change request filter to versions below 1.9.0 2025-09-23 18:00:14 +05:30
Jake BarnbyandGitHub a94d33649b Merge pull request #10493 from appwrite/fix-check-1.7.x
Move disable
2025-09-22 19:51:40 +12:00
Atharva Deosthale 7d4486b9e6 more fixes to tests 2025-09-19 18:42:25 +05:30
Atharva Deosthale 1a19e01d69 attempt to fix tests 2025-09-19 18:30:42 +05:30
Atharva Deosthale 2a145bc284 address reviews regarding tests 2025-09-19 18:18:44 +05:30
Atharva Deosthale 9fe3e94a66 e2e tests 2025-09-18 13:12:46 +05:30
Atharva Deosthale 8e32fb05d1 specs 2025-09-18 12:28:08 +05:30
Atharva Deosthale de8538b622 use request filters for 1.8.0 to convert into and 2025-09-18 12:27:24 +05:30
Jake Barnby 6d25eb81b6 Move disable 2025-09-15 13:09:08 +12:00
Atharva Deosthale fca2727966 add validation for type 2025-09-14 15:28:25 +05:30
Atharva Deosthale cb03bfe74c specs and sdk 2025-09-14 15:22:51 +05:30
Atharva Deosthale 2b1e3bd947 delete the new route and just do it in template route 2025-09-14 15:15:44 +05:30
Atharva Deosthale 4858317891 fix: format, lints 2025-09-13 18:02:13 +05:30
Atharva Deosthale 6e16340f16 feat: add branch deployments to appwrite 2025-09-13 17:56:19 +05:30
ArnabChatterjee20k ac604b11e3 added migration script 2025-09-01 13:13:52 +05:30
ArnabChatterjee20k f68d49c4d6 updated the migration error size attribute 2025-08-29 18:19:29 +05:30
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
731 changed files with 37087 additions and 12068 deletions
+3 -2
View File
@@ -22,7 +22,7 @@ _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_DOMAIN=appwrite.test
_APP_CONSOLE_DOMAIN=localhost
_APP_DOMAIN_FUNCTIONS=functions.localhost
_APP_DOMAIN_SITES=sites.localhost
@@ -103,6 +103,7 @@ _APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000
_APP_MAINTENANCE_RETENTION_SCHEDULES=86400
_APP_USAGE_STATS=enabled
_APP_LOGGING_CONFIG=
_APP_LOGGING_CONFIG_REALTIME=
_APP_GRAPHQL_MAX_BATCH_SIZE=10
_APP_GRAPHQL_MAX_COMPLEXITY=250
_APP_GRAPHQL_MAX_DEPTH=4
@@ -123,4 +124,4 @@ _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
+657 -383
View File
File diff suppressed because it is too large Load Diff
+60 -23
View File
@@ -1,44 +1,75 @@
---
on:
issues:
types: [opened, reopened]
schedule:
- cron: '0 0 * * *' # Run daily at midnight UTC
workflow_dispatch: # Enable manual trigger
stop-after: +30d # workflow will no longer trigger after 30 days. Remove this and recompile to run indefinitely
reaction: eyes
permissions: read-all
# 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 issue #${{ github.event.issue.number }} and perform some initial triage tasks related to that issue.
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. Select appropriate labels for the issue from the provided list.
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. 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 exit the workflow.
2. For each issue found, perform the following triage tasks:
3. Next, use the GitHub tools to gather additional context about the issue:
3. Use the `get_comments` tool to retrieve all the comments on the issue.
- Fetch the list of labels available in this repository. Use 'gh label list' bash command to fetch the labels. This will give you the labels you can use for triaging issues.
- Fetch any comments on the issue using the `get_issue_comments` tool
- Find similar issues if needed using the `search_issues` tool
- List the issues to see other open issues in the repository using the `list_issues` tool
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.
4. Analyze the issue content, considering:
5. Retrieve the issue content using the `get_issue` tool for any issues that pass the spam checks.
6. Next, use the GitHub tools to gather additional context about the issue:
- 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.)
@@ -47,32 +78,38 @@ You're a triage assistant for GitHub issues. Your task is to analyze issue #${{
- User impact
- Components affected
5. 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.
6. 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
7. 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
8. 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 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.
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
+25 -13
View File
@@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
--no-plugins --no-scripts --prefer-dist \
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
FROM appwrite/base:0.10.4 AS final
FROM appwrite/base:0.10.6 AS base
LABEL maintainer="team@appwrite.io"
@@ -24,11 +24,9 @@ ENV _APP_VERSION=$VERSION \
_APP_HOME=https://appwrite.io
RUN \
if [ "$DEBUG" == "true" ]; then \
if [ "$DEBUG" == "true" ]; then \
apk add boost boost-dev; \
fi
RUN apk add libwebp
fi
WORKDIR /usr/src/code
@@ -38,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 && \
@@ -92,14 +88,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" = "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
+23 -26
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,而且我们不会收集您的信用卡信息。
## 安装
@@ -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)!
+3 -16
View File
@@ -1,4 +1,4 @@
> We just announced Transactions API for Appwrite Databases - [Learn more](https://appwrite.io/blog/post/announcing-transactions-api)
> We just announced DB operators for Appwrite Databases - [Learn more](https://appwrite.io/blog/post/announcing-db-operators)
> Appwrite Cloud is now Generally Available - [Learn more](https://appwrite.io/cloud-ga)
@@ -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).
@@ -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" />
Binary file not shown.
Binary file not shown.
+2
View File
@@ -9,6 +9,7 @@ use Appwrite\Event\StatsResources;
use Appwrite\Event\StatsUsage;
use Appwrite\Platform\Appwrite;
use Appwrite\Runtimes\Runtimes;
use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Swoole\Runtime;
use Swoole\Timer;
@@ -76,6 +77,7 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) {
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', 'console');
$dbForPlatform->setDocumentType('users', User::class);
// Ensure tables exist
$collections = Config::getParam('collections', [])['console'];
+69 -3
View File
@@ -1,6 +1,6 @@
<?php
use Appwrite\Auth\Auth;
use Utopia\Auth\Hashes\Argon2;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
@@ -173,7 +173,7 @@ return [
'size' => 256,
'signed' => true,
'required' => false,
'default' => Auth::DEFAULT_ALGO,
'default' => (new Argon2())->getName(),
'array' => false,
'filters' => [],
],
@@ -184,7 +184,7 @@ return [
'size' => 65535,
'signed' => true,
'required' => false,
'default' => Auth::DEFAULT_ALGO_OPTIONS,
'default' => (new Argon2())->getOptions(),
'array' => false,
'filters' => ['json'],
],
@@ -364,6 +364,61 @@ return [
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('emailCanonical'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 320,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('emailIsFree'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('emailIsDisposable'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('emailIsCorporate'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('emailIsCanonical'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
@@ -1527,6 +1582,17 @@ return [
'required' => true,
'array' => false,
],
[
'$id' => ID::custom('transformations'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => false,
'array' => false,
'default' => true,
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
+1 -1
View File
@@ -2345,7 +2345,7 @@ return [
'$id' => ID::custom('errors'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 65535,
'size' => 1_000_000,
'signed' => true,
'required' => true,
'default' => null,
+1 -2
View File
@@ -4,7 +4,6 @@
* Initializes console project document.
*/
use Appwrite\Auth\Auth;
use Appwrite\Network\Platform;
use Utopia\Database\Helpers\ID;
use Utopia\System\System;
@@ -38,7 +37,7 @@ $console = [
'mockNumbers' => [],
'invites' => System::getEnv('_APP_CONSOLE_INVITES', 'enabled') === 'enabled',
'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
'duration' => TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled',
'invalidateSessions' => true
],
+42 -37
View File
@@ -522,6 +522,11 @@ return [
'description' => 'The requested file is not publicly readable.',
'code' => 403,
],
Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED => [
'name' => Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED,
'description' => 'Image transformations are disabled for the requested bucket.',
'code' => 403,
],
/** Tokens */
Exception::TOKEN_NOT_FOUND => [
@@ -683,12 +688,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 => [
@@ -705,41 +710,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 => [
@@ -759,7 +764,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 => [
@@ -776,7 +781,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 => [
@@ -786,7 +791,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 => [
@@ -796,7 +801,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 => [
@@ -813,17 +818,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 => [
@@ -838,12 +843,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 => [
@@ -853,7 +858,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 => [
@@ -864,7 +869,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,
],
@@ -878,17 +883,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 => [
@@ -903,12 +908,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 => [
@@ -918,7 +923,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 => [
@@ -928,24 +933,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 => [
@@ -955,24 +960,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 => [
@@ -982,19 +987,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 => [
+18 -24
View File
@@ -8,20 +8,13 @@ use Utopia\Config\Config;
$templateRuntimes = Config::getParam('template-runtimes');
function getVersions(array $versions, string $prefix)
{
return array_map(function ($version) use ($prefix) {
return $prefix . '-' . $version;
}, $versions);
}
return [
'analog' => [
'key' => 'analog',
'name' => 'Analog',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/analog/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/analog/env.sh',
'adapters' => [
@@ -47,7 +40,7 @@ return [
'name' => 'Angular',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/angular/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/angular/env.sh',
'adapters' => [
@@ -73,7 +66,7 @@ return [
'name' => 'Next.js',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/next-js/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/next-js/env.sh',
'adapters' => [
@@ -98,7 +91,7 @@ return [
'name' => 'React',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'runtimes' => $templateRuntimes['NODE'],
'adapters' => [
'static' => [
'key' => 'static',
@@ -115,7 +108,7 @@ return [
'name' => 'Nuxt',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/nuxt/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/nuxt/env.sh',
'adapters' => [
@@ -140,7 +133,7 @@ return [
'name' => 'Vue.js',
'screenshotSleep' => 5000,
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'runtimes' => $templateRuntimes['NODE'],
'adapters' => [
'static' => [
'key' => 'static',
@@ -157,7 +150,7 @@ return [
'name' => 'SvelteKit',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/sveltekit/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/sveltekit/env.sh',
'adapters' => [
@@ -182,7 +175,7 @@ return [
'name' => 'Astro',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/astro/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/astro/env.sh',
'adapters' => [
@@ -207,7 +200,7 @@ return [
'name' => 'TanStack Start',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/tanstack-start/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/tanstack-start/env.sh',
'adapters' => [
@@ -215,7 +208,7 @@ return [
'key' => 'ssr',
'buildCommand' => 'npm run build',
'installCommand' => 'npm install',
'outputDirectory' => './dist',
'outputDirectory' => './.output',
'startCommand' => 'bash helpers/tanstack-start/server.sh',
],
'static' => [
@@ -232,7 +225,7 @@ return [
'name' => 'Remix',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'runtimes' => $templateRuntimes['NODE'],
'bundleCommand' => 'bash /usr/local/server/helpers/remix/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/remix/env.sh',
'adapters' => [
@@ -257,7 +250,7 @@ return [
'name' => 'Lynx',
'screenshotSleep' => 5000,
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'runtimes' => $templateRuntimes['NODE'],
'adapters' => [
'static' => [
'key' => 'static',
@@ -273,8 +266,8 @@ return [
'key' => 'flutter',
'name' => 'Flutter',
'screenshotSleep' => 5000,
'buildRuntime' => 'flutter-3.29',
'runtimes' => getVersions($templateRuntimes['FLUTTER']['versions'], 'flutter'),
'buildRuntime' => 'flutter-3.35',
'runtimes' => $templateRuntimes['FLUTTER'],
'adapters' => [
'static' => [
'key' => 'static',
@@ -282,6 +275,7 @@ return [
'installCommand' => 'flutter pub get',
'outputDirectory' => './build/web',
'startCommand' => 'bash helpers/server.sh',
'fallbackFile' => 'index.html'
],
],
],
@@ -290,7 +284,7 @@ return [
'name' => 'React Native',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'runtimes' => $templateRuntimes['NODE'],
'adapters' => [
'static' => [
'key' => 'static',
@@ -307,7 +301,7 @@ return [
'name' => 'Vite',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'runtimes' => $templateRuntimes['NODE'],
'adapters' => [
'static' => [
'key' => 'static',
@@ -323,7 +317,7 @@ return [
'name' => 'Other',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
'runtimes' => $templateRuntimes['NODE'],
'adapters' => [
'static' => [
'key' => 'static',
+25
View File
@@ -0,0 +1,25 @@
<?php
use Utopia\System\System;
/**
* 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,
];
+7 -7
View File
@@ -1,6 +1,6 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Utopia\Database\Documents\User;
$member = [
'global',
@@ -92,7 +92,7 @@ $admins = [
];
return [
Auth::USER_ROLE_GUESTS => [
User::ROLE_GUESTS => [
'label' => 'Guests',
'scopes' => [
'global',
@@ -112,23 +112,23 @@ return [
'execution.write',
],
],
Auth::USER_ROLE_USERS => [
User::ROLE_USERS => [
'label' => 'Users',
'scopes' => \array_merge($member),
],
Auth::USER_ROLE_ADMIN => [
User::ROLE_ADMIN => [
'label' => 'Admin',
'scopes' => \array_merge($admins),
],
Auth::USER_ROLE_DEVELOPER => [
User::ROLE_DEVELOPER => [
'label' => 'Developer',
'scopes' => \array_merge($admins),
],
Auth::USER_ROLE_OWNER => [
User::ROLE_OWNER => [
'label' => 'Owner',
'scopes' => \array_merge($member, $admins),
],
Auth::USER_ROLE_APPS => [
User::ROLE_APPS => [
'label' => 'Applications',
'scopes' => ['global', 'health.read', 'graphql'],
],
+2 -2
View File
@@ -47,10 +47,10 @@ return [ // List of publicly visible scopes
'description' => 'Access to create, update, and delete your project\'s database table\'s columns',
],
'indexes.read' => [
'description' => 'Access to read your project\'s database collection\'s indexes',
'description' => 'Access to read your project\'s database table\'s indexes',
],
'indexes.write' => [
'description' => 'Access to create, update, and delete your project\'s database collection\'s indexes',
'description' => 'Access to create, update, and delete your project\'s database table\'s indexes',
],
'documents.read' => [
'description' => 'Access to read your project\'s database documents',
@@ -11,7 +11,7 @@ return [
[
'key' => 'web',
'name' => 'Web',
'version' => '21.4.0',
'version' => '21.5.0',
'url' => 'https://github.com/appwrite/sdk-for-web',
'package' => 'https://www.npmjs.com/package/appwrite',
'enabled' => true,
@@ -60,7 +60,7 @@ return [
[
'key' => 'flutter',
'name' => 'Flutter',
'version' => '20.3.0',
'version' => '20.3.2',
'url' => 'https://github.com/appwrite/sdk-for-flutter',
'package' => 'https://pub.dev/packages/appwrite',
'enabled' => true,
@@ -79,7 +79,7 @@ return [
[
'key' => 'apple',
'name' => 'Apple',
'version' => '13.4.0',
'version' => '13.5.0',
'url' => 'https://github.com/appwrite/sdk-for-apple',
'package' => 'https://github.com/appwrite/sdk-for-apple',
'enabled' => true,
@@ -116,7 +116,7 @@ return [
[
'key' => 'android',
'name' => 'Android',
'version' => '11.3.0',
'version' => '11.4.0',
'url' => 'https://github.com/appwrite/sdk-for-android',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-android',
'enabled' => true,
@@ -139,7 +139,7 @@ return [
[
'key' => 'react-native',
'name' => 'React Native',
'version' => '0.18.0',
'version' => '0.19.0',
'url' => 'https://github.com/appwrite/sdk-for-react-native',
'package' => 'https://npmjs.com/package/react-native-appwrite',
'enabled' => true,
@@ -226,7 +226,7 @@ return [
[
'key' => 'cli',
'name' => 'Command Line',
'version' => '11.1.0',
'version' => '12.0.1',
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
@@ -262,7 +262,7 @@ return [
[
'key' => 'nodejs',
'name' => 'Node.js',
'version' => '20.3.0',
'version' => '21.0.0',
'url' => 'https://github.com/appwrite/sdk-for-node',
'package' => 'https://www.npmjs.com/package/node-appwrite',
'enabled' => true,
@@ -281,7 +281,7 @@ return [
[
'key' => 'php',
'name' => 'PHP',
'version' => '17.5.0',
'version' => '19.0.0',
'url' => 'https://github.com/appwrite/sdk-for-php',
'package' => 'https://packagist.org/packages/appwrite/appwrite',
'enabled' => true,
@@ -300,7 +300,7 @@ return [
[
'key' => 'python',
'name' => 'Python',
'version' => '13.6.1',
'version' => '14.0.0',
'url' => 'https://github.com/appwrite/sdk-for-python',
'package' => 'https://pypi.org/project/appwrite/',
'enabled' => true,
@@ -319,7 +319,7 @@ return [
[
'key' => 'ruby',
'name' => 'Ruby',
'version' => '19.3.0',
'version' => '20.0.0',
'url' => 'https://github.com/appwrite/sdk-for-ruby',
'package' => 'https://rubygems.org/gems/appwrite',
'enabled' => true,
@@ -338,7 +338,7 @@ return [
[
'key' => 'go',
'name' => 'Go',
'version' => 'v0.14.0',
'version' => 'v0.15.0',
'url' => 'https://github.com/appwrite/sdk-for-go',
'package' => 'https://github.com/appwrite/sdk-for-go',
'enabled' => true,
@@ -357,7 +357,7 @@ return [
[
'key' => 'dotnet',
'name' => '.NET',
'version' => '0.22.0',
'version' => '0.23.0',
'url' => 'https://github.com/appwrite/sdk-for-dotnet',
'package' => 'https://www.nuget.org/packages/Appwrite',
'enabled' => true,
@@ -376,7 +376,7 @@ return [
[
'key' => 'dart',
'name' => 'Dart',
'version' => '19.3.0',
'version' => '20.0.0',
'url' => 'https://github.com/appwrite/sdk-for-dart',
'package' => 'https://pub.dev/packages/dart_appwrite',
'enabled' => true,
@@ -395,7 +395,7 @@ return [
[
'key' => 'kotlin',
'name' => 'Kotlin',
'version' => '12.3.0',
'version' => '13.0.0',
'url' => 'https://github.com/appwrite/sdk-for-kotlin',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-kotlin',
'enabled' => true,
@@ -418,7 +418,7 @@ return [
[
'key' => 'swift',
'name' => 'Swift',
'version' => '13.3.0',
'version' => '14.0.0',
'url' => 'https://github.com/appwrite/sdk-for-swift',
'package' => 'https://github.com/appwrite/sdk-for-swift',
'enabled' => true,
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
+9 -40
View File
@@ -1,43 +1,12 @@
<?php
// TODO: Remove, replace with runtimes.php directly
// Used in function templates and site frameworks
use Utopia\Config\Config;
return [
'NODE' => [
'name' => 'node',
'versions' => ['22', '21.0', '20.0', '19.0', '18.0', '16.0', '14.5']
],
'PYTHON' => [
'name' => 'python',
'versions' => ['3.12', '3.11', '3.10', '3.9', '3.8']
],
'DART' => [
'name' => 'dart',
'versions' => ['3.8', '3.5', '3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16']
],
'GO' => [
'name' => 'go',
'versions' => ['1.23']
],
'PHP' => [
'name' => 'php',
'versions' => ['8.3', '8.2', '8.1', '8.0']
],
'DENO' => [
'name' => 'deno',
'versions' => ['2.0', '1.46', '1.40', '1.35', '1.24', '1.21']
],
'BUN' => [
'name' => 'bun',
'versions' => ['1.1', '1.0']
],
'RUBY' => [
'name' => 'ruby',
'versions' => ['3.3', '3.2', '3.1', '3.0']
],
'FLUTTER' => [
'name' => 'flutter',
'versions' => ['3.32', '3.24']
],
];
$runtimes = Config::getParam('runtimes');
$mappedRuntimes = \array_reduce($runtimes, function ($acc, $runtime) {
$acc[strtoupper($runtime['key'])][] = $runtime['key'] . '-' . $runtime['version'];
return $acc;
}, []);
return $mappedRuntimes;
+133 -72
View File
@@ -1,20 +1,22 @@
<?php
use Utopia\Config\Config;
use Utopia\System\System;
$templateRuntimes = Config::getParam('template-runtimes');
$allowList = \array_map('trim', \explode(',', System::getEnv('_APP_FUNCTIONS_RUNTIMES', '')));
function getRuntimes($runtime, $commands, $entrypoint, $providerRootDirectory, $versionsDenyList = [])
function getRuntimes($runtimes, $commands, $entrypoint, $providerRootDirectory, $allowList)
{
return array_map(function ($version) use ($runtime, $commands, $entrypoint, $providerRootDirectory) {
return array_map(function ($runtime) use ($commands, $entrypoint, $providerRootDirectory) {
return [
'name' => $runtime['name'] . '-' . $version,
'name' => $runtime,
'commands' => $commands,
'entrypoint' => $entrypoint,
'providerRootDirectory' => $providerRootDirectory
];
}, array_filter($runtime['versions'], function ($version) use ($versionsDenyList) {
return !in_array($version, $versionsDenyList);
}, array_filter($runtimes, function ($runtime) use ($allowList) {
return in_array($runtime, $allowList);
}));
}
@@ -32,24 +34,26 @@ return [
'timeout' => 15,
'useCases' => ['starter'],
'runtimes' => [
...getRuntimes($templateRuntimes['NODE'], 'npm install', 'src/main.js', 'node/starter'),
...getRuntimes($templateRuntimes['NODE'], 'npm install', 'src/main.js', 'node/starter', $allowList),
...getRuntimes(
$templateRuntimes['PYTHON'],
'pip install -r requirements.txt',
'src/main.py',
'python/starter'
'python/starter',
$allowList
),
...getRuntimes($templateRuntimes['DART'], 'dart pub get', 'lib/main.dart', 'dart/starter'),
...getRuntimes($templateRuntimes['GO'], '', 'main.go', 'go/starter'),
...getRuntimes($templateRuntimes['DART'], 'dart pub get', 'lib/main.dart', 'dart/starter', $allowList),
...getRuntimes($templateRuntimes['GO'], '', 'main.go', 'go/starter', $allowList),
...getRuntimes(
$templateRuntimes['PHP'],
'composer install',
'src/index.php',
'php/starter'
'php/starter',
$allowList
),
...getRuntimes($templateRuntimes['DENO'], 'deno cache src/main.ts', 'src/main.ts', 'deno/starter'),
...getRuntimes($templateRuntimes['BUN'], 'bun install', 'src/main.ts', 'bun/starter'),
...getRuntimes($templateRuntimes['RUBY'], 'bundle install', 'lib/main.rb', 'ruby/starter'),
...getRuntimes($templateRuntimes['DENO'], 'deno cache src/main.ts', 'src/main.ts', 'deno/starter', $allowList),
...getRuntimes($templateRuntimes['BUN'], 'bun install', 'src/main.ts', 'bun/starter', $allowList),
...getRuntimes($templateRuntimes['RUBY'], 'bundle install', 'lib/main.rb', 'ruby/starter', $allowList),
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/starter">file</a>.',
'vcsProvider' => 'github',
@@ -75,7 +79,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/query-upstash-vector'
'node/query-upstash-vector',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/query-upstash-vector">file</a>.',
@@ -120,7 +125,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/query-redis-labs'
'node/query-redis-labs',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/query-redis-labs">file</a>.',
@@ -164,7 +170,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/query-neo4j-auradb'
'node/query-neo4j-auradb',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/query-neo4j-auradb">file</a>.',
@@ -217,7 +224,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/query-mongo-atlas'
'node/query-mongo-atlas',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/query-mongo-atlas">file</a>.',
@@ -255,7 +263,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/query-neon-postgres'
'node/query-neon-postgres',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/query-neon-postgres">file</a>.',
@@ -323,25 +332,29 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/prompt-chatgpt'
'node/prompt-chatgpt',
$allowList
),
...getRuntimes(
$templateRuntimes['PYTHON'],
'pip install -r requirements.txt',
'src/main.py',
'python/prompt_chatgpt'
'python/prompt_chatgpt',
$allowList
),
...getRuntimes(
$templateRuntimes['PHP'],
'composer install',
'src/index.php',
'php/prompt-chatgpt'
'php/prompt-chatgpt',
$allowList
),
...getRuntimes(
$templateRuntimes['DART'],
'dart pub get',
'lib/main.dart',
'dart/prompt_chatgpt'
'dart/prompt_chatgpt',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/prompt-chatgpt">file</a>.',
@@ -385,19 +398,22 @@ return [
$templateRuntimes['NODE'],
'npm install && npm run setup',
'src/main.js',
'node/discord-command-bot'
'node/discord-command-bot',
$allowList
),
...getRuntimes(
$templateRuntimes['PYTHON'],
'pip install -r requirements.txt && python src/setup.py',
'src/main.py',
'python/discord_command_bot'
'python/discord_command_bot',
$allowList
),
...getRuntimes(
$templateRuntimes['GO'],
'',
'main.go',
'go/discord-command-bot'
'go/discord-command-bot',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/discord-command-bot">file</a>.',
@@ -449,7 +465,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/analyze-with-perspectiveapi'
'node/analyze-with-perspectiveapi',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/analyze-with-perspectiveapi">file</a>.',
@@ -486,19 +503,22 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/censor-with-redact'
'node/censor-with-redact',
$allowList
),
...getRuntimes(
$templateRuntimes['PYTHON'],
'pip install -r requirements.txt',
'src/main.py',
'python/censor_with_redact'
'python/censor_with_redact',
$allowList
),
...getRuntimes(
$templateRuntimes['DART'],
'dart pub get',
'lib/main.dart',
'dart/censor_with_redact'
'dart/censor_with_redact',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/censor-with-redact">file</a>.',
@@ -530,7 +550,7 @@ return [
'timeout' => 15,
'useCases' => ['utilities'],
'runtimes' => [
...getRuntimes($templateRuntimes['NODE'], 'npm install', 'src/main.js', 'node/generate-pdf')
...getRuntimes($templateRuntimes['NODE'], 'npm install', 'src/main.js', 'node/generate-pdf', $allowList)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/generate-pdf">file</a>.',
'vcsProvider' => 'github',
@@ -557,7 +577,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/github-issue-bot'
'node/github-issue-bot',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/github-issue-bot">file</a>.',
@@ -601,7 +622,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/url-shortener'
'node/url-shortener',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/url-shortener">file</a>.',
@@ -653,19 +675,22 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/sync-with-algolia'
'node/sync-with-algolia',
$allowList
),
...getRuntimes(
$templateRuntimes['PYTHON'],
'pip install -r requirements.txt',
'src/main.py',
'python/sync_with_algolia'
'python/sync_with_algolia',
$allowList
),
...getRuntimes(
$templateRuntimes['PHP'],
'composer install',
'src/index.php',
'php/sync-with-algolia'
'php/sync-with-algolia',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/sync-with-algolia">file</a>.',
@@ -735,31 +760,36 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/sync-with-meilisearch'
'node/sync-with-meilisearch',
$allowList
),
...getRuntimes(
$templateRuntimes['PYTHON'],
'pip install -r requirements.txt',
'src/main.py',
'python/sync-with-meilisearch'
'python/sync-with-meilisearch',
$allowList
),
...getRuntimes(
$templateRuntimes['PHP'],
'composer install',
'src/index.php',
'php/sync-with-meilisearch'
'php/sync-with-meilisearch',
$allowList
),
...getRuntimes(
$templateRuntimes['BUN'],
'bun install',
'src/main.ts',
'bun/sync-with-meilisearch'
'bun/sync-with-meilisearch',
$allowList
),
...getRuntimes(
$templateRuntimes['RUBY'],
'bundle install',
'lib/main.rb',
'ruby/sync-with-meilisearch'
'ruby/sync-with-meilisearch',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/sync-with-meilisearch">file</a>.',
@@ -829,37 +859,43 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/whatsapp-with-vonage'
'node/whatsapp-with-vonage',
$allowList
),
...getRuntimes(
$templateRuntimes['PYTHON'],
'pip install -r requirements.txt',
'src/main.py',
'python/whatsapp_with_vonage'
'python/whatsapp_with_vonage',
$allowList
),
...getRuntimes(
$templateRuntimes['DART'],
'dart pub get',
'lib/main.dart',
'dart/whatsapp-with-vonage'
'dart/whatsapp-with-vonage',
$allowList
),
...getRuntimes(
$templateRuntimes['PHP'],
'composer install',
'src/index.php',
'php/whatsapp-with-vonage'
'php/whatsapp-with-vonage',
$allowList
),
...getRuntimes(
$templateRuntimes['BUN'],
'bun install',
'src/main.ts',
'bun/whatsapp-with-vonage'
'bun/whatsapp-with-vonage',
$allowList
),
...getRuntimes(
$templateRuntimes['RUBY'],
'bundle install',
'lib/main.rb',
'ruby/whatsapp-with-vonage'
'ruby/whatsapp-with-vonage',
$allowList
),
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/whatsapp-with-vonage">file</a>.',
@@ -916,7 +952,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/push-notification-with-fcm'
'node/push-notification-with-fcm',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/push-notification-with-fcm">file</a>.',
@@ -973,19 +1010,22 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/email-contact-form'
'node/email-contact-form',
$allowList
),
...getRuntimes(
$templateRuntimes['PYTHON'],
'pip install -r requirements.txt',
'src/main.py',
'python/email_contact_form'
'python/email_contact_form',
$allowList
),
...getRuntimes(
$templateRuntimes['PHP'],
'composer install',
'src/index.php',
'php/email-contact-form'
'php/email-contact-form',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/email-contact-form">file</a>.',
@@ -1057,7 +1097,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/subscriptions-with-stripe'
'node/subscriptions-with-stripe',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/subscriptions-with-stripe">file</a>.',
@@ -1099,7 +1140,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/payments-with-stripe'
'node/payments-with-stripe',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/payments-with-stripe">file</a>.',
@@ -1157,7 +1199,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/text-generation-with-huggingface'
'node/text-generation-with-huggingface',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/text-generation-with-huggingface">file</a>.',
@@ -1192,7 +1235,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/language-translation-with-huggingface'
'node/language-translation-with-huggingface',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/language-translation-with-huggingface">file</a>.',
@@ -1227,7 +1271,8 @@ return [
$templateRuntimes['NODE'],
'npm install && npm run setup',
'src/main.js',
'node/image-classification-with-huggingface'
'node/image-classification-with-huggingface',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/image-classification-with-huggingface">file</a>.',
@@ -1286,7 +1331,8 @@ return [
$templateRuntimes['NODE'],
'npm install && npm run setup',
'src/main.js',
'node/object-detection-with-huggingface'
'node/object-detection-with-huggingface',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/object-detection-with-huggingface">file</a>.',
@@ -1345,7 +1391,8 @@ return [
$templateRuntimes['NODE'],
'npm install && npm run setup',
'src/main.js',
'node/speech-recognition-with-huggingface'
'node/speech-recognition-with-huggingface',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/speech-recognition-with-huggingface">file</a>.',
@@ -1407,7 +1454,8 @@ return [
$templateRuntimes['NODE'],
'npm install && npm run setup',
'src/main.js',
'node/text-to-speech-with-huggingface'
'node/text-to-speech-with-huggingface',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/text-to-speech-with-huggingface">file</a>.',
@@ -1466,7 +1514,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/generate-with-replicate'
'node/generate-with-replicate',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/generate-with-replicate">file</a>.',
@@ -1502,7 +1551,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/generate-with-together-ai'
'node/generate-with-together-ai',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/generate-with-together-ai">file</a>.',
@@ -1545,7 +1595,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/chat-with-perplexity-ai'
'node/chat-with-perplexity-ai',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/chat-with-perplexity-ai">file</a>.',
@@ -1587,7 +1638,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/generate-with-replicate'
'node/generate-with-replicate',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/generate-with-replicate">file</a>.',
@@ -1623,7 +1675,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/sync-with-pinecone'
'node/sync-with-pinecone',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/sync-with-pinecone">file</a>.',
@@ -1687,7 +1740,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/rag-with-langchain'
'node/rag-with-langchain',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/rag-with-langchain">file</a>.',
@@ -1751,7 +1805,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/speak-with-elevenlabs'
'node/speak-with-elevenlabs',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/speak-with-elevenlabs">file</a>.',
@@ -1807,7 +1862,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/speak-with-lmnt'
'node/speak-with-lmnt',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/speak-with-lmnt">file</a>.',
@@ -1849,7 +1905,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/chat-with-anyscale'
'node/chat-with-anyscale',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/chat-with-anyscale">file</a>.',
@@ -1891,7 +1948,8 @@ return [
$templateRuntimes['NODE'],
'npm install && npm run setup',
'src/main.js',
'node/music-generation-with-huggingface'
'node/music-generation-with-huggingface',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/music-generation-with-huggingface">file</a>.',
@@ -1934,7 +1992,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/generate-with-fal-ai'
'node/generate-with-fal-ai',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/generate-with-fal-ai">file</a>.',
@@ -1970,7 +2029,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/subscriptions-with-lemon-squeezy'
'node/subscriptions-with-lemon-squeezy',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/subscriptions-with-lemon-squeezy">file</a>.',
@@ -2026,7 +2086,8 @@ return [
$templateRuntimes['NODE'],
'npm install',
'src/main.js',
'node/payments-with-lemon-squeezy'
'node/payments-with-lemon-squeezy',
$allowList
)
],
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/payments-with-lemon-squeezy">file</a>.',
@@ -2094,7 +2155,7 @@ return [
'timeout' => 15,
'useCases' => ['auth'],
'runtimes' => [
...getRuntimes($templateRuntimes['DART'], 'dart pub get', 'lib/main.dart', 'dart/sign_in_with_apple')
...getRuntimes($templateRuntimes['DART'], 'dart pub get', 'lib/main.dart', 'dart/sign_in_with_apple', $allowList)
],
'instructions' => 'For documentation and instructions, check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/dart/sign_in_with_apple">file</a>.',
'vcsProvider' => 'github',
+167 -8
View File
@@ -1,5 +1,6 @@
<?php
use Utopia\Config\Config;
use Utopia\System\System;
/**
@@ -7,12 +8,8 @@ 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;
@@ -24,6 +21,9 @@ class UseCases
public const ECOMMERCE = 'ecommerce';
public const DOCUMENTATION = 'documentation';
public const BLOG = 'blog';
public const AI = 'artificial intelligence';
public const FORMS = 'forms';
public const DASHBOARD = 'dashboard';
}
const TEMPLATE_FRAMEWORKS = [
@@ -83,7 +83,7 @@ const TEMPLATE_FRAMEWORKS = [
'installCommand' => '',
'buildCommand' => 'flutter build web',
'outputDirectory' => './build/web',
'buildRuntime' => 'flutter-3.29',
'buildRuntime' => 'flutter-3.35',
'adapter' => 'static',
'fallbackFile' => '',
],
@@ -970,7 +970,7 @@ return [
'name' => 'TanStack Start starter',
'useCases' => [UseCases::STARTER],
'tagline' => 'Simple TanStack Start application integrated with Appwrite SDK.',
'score' => 6, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'score' => 9, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-tanstack-start-dark.png',
'screenshotLight' => $url . '/images/sites/templates/starter-for-tanstack-start-light.png',
'frameworks' => [
@@ -1443,4 +1443,163 @@ return [
'providerVersion' => '0.3.*',
'variables' => []
],
[
'key' => 'text-to-speech',
'name' => 'Text-to-speech with ElevenLabs',
'tagline' => 'Next.js app that transforms text into natural, human-like speech using ElevenLabs',
'score' => 10, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::AI],
'screenshotDark' => $url . '/images/sites/templates/text-to-speech-dark.png',
'screenshotLight' => $url . '/images/sites/templates/text-to-speech-light.png',
'frameworks' => [
getFramework('NEXTJS', [
'providerRootDirectory' => './nextjs/text-to-speech',
]),
],
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.6.*',
'variables' => [
[
'name' => 'ELEVENLABS_API_KEY',
'description' => 'Your ElevenLabs API key',
'value' => '',
'placeholder' => 'sk_.....',
'required' => true,
'type' => 'password'
],
]
],
[
'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' => [UseCases::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' => [UseCases::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'
],
]
]
];
+1 -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,
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -96,8 +96,8 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
$className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider);
$oAuthProviders = Config::getParam('oAuthProviders');
$className = $oAuthProviders[$provider]['class'];
if (!\class_exists($className)) {
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
@@ -662,26 +662,26 @@ App::get('/v1/avatars/screenshots')
],
contentType: ContentType::IMAGE_PNG
))
->param('url', '', new URL(['http', 'https']), 'Website URL which you want to capture.')
->param('headers', [], new Assoc(), 'HTTP headers to send with the browser request. Defaults to empty.', true)
->param('viewportWidth', 1280, new Range(1, 1920), 'Browser viewport width. Pass an integer between 1 to 1920. Defaults to 1280.', true)
->param('viewportHeight', 720, new Range(1, 1080), 'Browser viewport height. Pass an integer between 1 to 1080. Defaults to 720.', true)
->param('scale', 1, new Range(0.1, 3, Range::TYPE_FLOAT), 'Browser scale factor. Pass a number between 0.1 to 3. Defaults to 1.', true)
->param('theme', 'light', new WhiteList(['light', 'dark']), 'Browser theme. Pass "light" or "dark". Defaults to "light".', true)
->param('userAgent', '', new Text(512), 'Custom user agent string. Defaults to browser default.', true)
->param('fullpage', false, new Boolean(true), 'Capture full page scroll. Pass 0 for viewport only, or 1 for full page. Defaults to 0.', true)
->param('locale', '', new Text(10), 'Browser locale (e.g., "en-US", "fr-FR"). Defaults to browser default.', true)
->param('timezone', '', new WhiteList(timezone_identifiers_list()), 'IANA timezone identifier (e.g., "America/New_York", "Europe/London"). Defaults to browser default.', true)
->param('latitude', 0, new Range(-90, 90, Range::TYPE_FLOAT), 'Geolocation latitude. Pass a number between -90 to 90. Defaults to 0.', true)
->param('longitude', 0, new Range(-180, 180, Range::TYPE_FLOAT), 'Geolocation longitude. Pass a number between -180 to 180. Defaults to 0.', true)
->param('accuracy', 0, new Range(0, 100000, Range::TYPE_FLOAT), 'Geolocation accuracy in meters. Pass a number between 0 to 100000. Defaults to 0.', true)
->param('touch', false, new Boolean(true), 'Enable touch support. Pass 0 for no touch, or 1 for touch enabled. Defaults to 0.', true)
->param('permissions', [], new ArrayList(new WhiteList(['geolocation', 'camera', 'microphone', 'notifications', 'midi', 'push', 'clipboard-read', 'clipboard-write', 'payment-handler', 'usb', 'bluetooth', 'accelerometer', 'gyroscope', 'magnetometer', 'ambient-light-sensor', 'background-sync', 'persistent-storage', 'screen-wake-lock', 'web-share', 'xr-spatial-tracking'])), 'Browser permissions to grant. Pass an array of permission names like ["geolocation", "camera", "microphone"]. Defaults to empty.', true)
->param('sleep', 0, new Range(0, 10), 'Wait time in seconds before taking the screenshot. Pass an integer between 0 to 10. Defaults to 0.', true)
->param('width', 0, new Range(0, 2000), 'Output image width. Pass 0 to use original width, or an integer between 1 to 2000. Defaults to 0 (original width).', true)
->param('height', 0, new Range(0, 2000), 'Output image height. Pass 0 to use original height, or an integer between 1 to 2000. Defaults to 0 (original height).', true)
->param('quality', -1, new Range(-1, 100), 'Screenshot quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true)
->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true)
->param('url', '', new URL(['http', 'https']), 'Website URL which you want to capture.', example: 'https://example.com')
->param('headers', [], new Assoc(), 'HTTP headers to send with the browser request. Defaults to empty.', true, example: '{"Authorization":"Bearer token123","X-Custom-Header":"value"}')
->param('viewportWidth', 1280, new Range(1, 1920), 'Browser viewport width. Pass an integer between 1 to 1920. Defaults to 1280.', true, example: '1920')
->param('viewportHeight', 720, new Range(1, 1080), 'Browser viewport height. Pass an integer between 1 to 1080. Defaults to 720.', true, example: '1080')
->param('scale', 1, new Range(0.1, 3, Range::TYPE_FLOAT), 'Browser scale factor. Pass a number between 0.1 to 3. Defaults to 1.', true, example: '2')
->param('theme', 'light', new WhiteList(['light', 'dark']), 'Browser theme. Pass "light" or "dark". Defaults to "light".', true, example: 'dark')
->param('userAgent', '', new Text(512), 'Custom user agent string. Defaults to browser default.', true, example: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15')
->param('fullpage', false, new Boolean(true), 'Capture full page scroll. Pass 0 for viewport only, or 1 for full page. Defaults to 0.', true, example: 'true')
->param('locale', '', new Text(10), 'Browser locale (e.g., "en-US", "fr-FR"). Defaults to browser default.', true, example: 'en-US')
->param('timezone', '', new WhiteList(timezone_identifiers_list()), 'IANA timezone identifier (e.g., "America/New_York", "Europe/London"). Defaults to browser default.', true, example: 'america/new_york')
->param('latitude', 0, new Range(-90, 90, Range::TYPE_FLOAT), 'Geolocation latitude. Pass a number between -90 to 90. Defaults to 0.', true, example: '37.7749')
->param('longitude', 0, new Range(-180, 180, Range::TYPE_FLOAT), 'Geolocation longitude. Pass a number between -180 to 180. Defaults to 0.', true, example: '-122.4194')
->param('accuracy', 0, new Range(0, 100000, Range::TYPE_FLOAT), 'Geolocation accuracy in meters. Pass a number between 0 to 100000. Defaults to 0.', true, example: '100')
->param('touch', false, new Boolean(true), 'Enable touch support. Pass 0 for no touch, or 1 for touch enabled. Defaults to 0.', true, example: 'true')
->param('permissions', [], new ArrayList(new WhiteList(['geolocation', 'camera', 'microphone', 'notifications', 'midi', 'push', 'clipboard-read', 'clipboard-write', 'payment-handler', 'usb', 'bluetooth', 'accelerometer', 'gyroscope', 'magnetometer', 'ambient-light-sensor', 'background-sync', 'persistent-storage', 'screen-wake-lock', 'web-share', 'xr-spatial-tracking'])), 'Browser permissions to grant. Pass an array of permission names like ["geolocation", "camera", "microphone"]. Defaults to empty.', true, example: '["geolocation","notifications"]')
->param('sleep', 0, new Range(0, 10), 'Wait time in seconds before taking the screenshot. Pass an integer between 0 to 10. Defaults to 0.', true, example: '3')
->param('width', 0, new Range(0, 2000), 'Output image width. Pass 0 to use original width, or an integer between 1 to 2000. Defaults to 0 (original width).', true, example: '800')
->param('height', 0, new Range(0, 2000), 'Output image height. Pass 0 to use original height, or an integer between 1 to 2000. Defaults to 0 (original height).', true, example: '600')
->param('quality', -1, new Range(-1, 100), 'Screenshot quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true, example: '85')
->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true, example: 'jpeg')
->inject('response')
->inject('queueForStatsUsage')
->action(function (string $url, array $headers, int $viewportWidth, int $viewportHeight, float $scale, string $theme, string $userAgent, bool $fullpage, string $locale, string $timezone, float $latitude, float $longitude, float $accuracy, bool $touch, array $permissions, int $sleep, int $width, int $height, int $quality, string $output, Response $response, StatsUsage $queueForStatsUsage) {
+1 -6
View File
@@ -43,9 +43,6 @@ App::get('/v1/console/variables')
))
->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();
@@ -55,9 +52,7 @@ App::get('/v1/console/variables')
$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
);
$isDomainEnabled = $isAAAAValid || $isAValid || $isCNAMEValid;
$isVcsEnabled = !empty(System::getEnv('_APP_VCS_GITHUB_APP_NAME', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY', ''))
+2 -2
View File
@@ -1,6 +1,5 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\GraphQL\Promises\Adapter;
@@ -9,6 +8,7 @@ use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use GraphQL\Error\DebugFlag;
@@ -32,7 +32,7 @@ App::init()
if (
array_key_exists('graphql', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['graphql']
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
-5
View File
@@ -37,7 +37,6 @@ App::get('/v1/locale')
$currencies = Config::getParam('locale-currencies');
$output = [];
$ip = $request->getIP();
$time = (60 * 60 * 24 * 45); // 45 days cache
$output['ip'] = $ip;
@@ -68,10 +67,6 @@ App::get('/v1/locale')
$output['currency'] = $currency;
}
$response
->addHeader('Cache-Control', 'public, max-age=' . $time)
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
;
$response->dynamic(new Document($output), Response::MODEL_LOCALE);
});
+85 -102
View File
@@ -49,6 +49,7 @@ use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\JSON;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@@ -80,12 +81,12 @@ App::post('/v1/messaging/providers/mailgun')
->param('name', '', new Text(128), 'Provider name.')
->param('apiKey', '', new Text(0), 'Mailgun API Key.', true)
->param('domain', '', new Text(0), 'Mailgun Domain.', true)
->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true)
->param('isEuRegion', null, new Nullable(new Boolean()), 'Set as EU region.', true)
->param('fromName', '', new Text(128, 0), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128, 0), 'Name set in the reply to field for the mail. Default value is sender name. Reply to name must have reply to email as well.', true)
->param('replyToEmail', '', new Email(), 'Email set in the reply to field for the mail. Default value is sender email. Reply to email must have reply to name as well.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -177,7 +178,7 @@ App::post('/v1/messaging/providers/sendgrid')
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128, 0), 'Name set in the reply to field for the mail. Default value is sender name.', true)
->param('replyToEmail', '', new Email(), 'Email set in the reply to field for the mail. Default value is sender email.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -259,7 +260,7 @@ App::post('/v1/messaging/providers/resend')
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128, 0), 'Name set in the reply to field for the mail. Default value is sender name.', true)
->param('replyToEmail', '', new Email(), 'Email set in the reply to field for the mail. Default value is sender email.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -338,6 +339,7 @@ App::post('/v1/messaging/providers/smtp')
since: '1.8.0',
replaceWith: 'messaging.createSMTPProvider',
),
public: false,
),
new Method(
namespace: 'messaging',
@@ -366,7 +368,7 @@ App::post('/v1/messaging/providers/smtp')
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128, 0), 'Name set in the reply to field for the mail. Default value is sender name.', true)
->param('replyToEmail', '', new Email(), 'Email set in the reply to field for the mail. Default value is sender email.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -453,7 +455,7 @@ App::post('/v1/messaging/providers/msg91')
->param('templateId', '', new Text(0), 'Msg91 template ID', true)
->param('senderId', '', new Text(0), 'Msg91 sender ID.', true)
->param('authKey', '', new Text(0), 'Msg91 auth key.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -536,7 +538,7 @@ App::post('/v1/messaging/providers/telesign')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('customerId', '', new Text(0), 'Telesign customer ID.', true)
->param('apiKey', '', new Text(0), 'Telesign API key.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -620,7 +622,7 @@ App::post('/v1/messaging/providers/textmagic')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('username', '', new Text(0), 'Textmagic username.', true)
->param('apiKey', '', new Text(0), 'Textmagic apiKey.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -704,7 +706,7 @@ App::post('/v1/messaging/providers/twilio')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('accountSid', '', new Text(0), 'Twilio account secret ID.', true)
->param('authToken', '', new Text(0), 'Twilio authentication token.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -788,7 +790,7 @@ App::post('/v1/messaging/providers/vonage')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('apiKey', '', new Text(0), 'Vonage API key.', true)
->param('apiSecret', '', new Text(0), 'Vonage API secret.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -871,6 +873,7 @@ App::post('/v1/messaging/providers/fcm')
since: '1.8.0',
replaceWith: 'messaging.createFCMProvider',
),
public: false,
),
new Method(
namespace: 'messaging',
@@ -888,8 +891,8 @@ App::post('/v1/messaging/providers/fcm')
])
->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('name', '', new Text(128), 'Provider name.')
->param('serviceAccountJSON', null, new JSON(), 'FCM service account JSON.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('serviceAccountJSON', null, new Nullable(new JSON()), 'FCM service account JSON.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -960,6 +963,7 @@ App::post('/v1/messaging/providers/apns')
since: '1.8.0',
replaceWith: 'messaging.createAPNSProvider',
),
public: false,
),
new Method(
namespace: 'messaging',
@@ -982,7 +986,7 @@ App::post('/v1/messaging/providers/apns')
->param('teamId', '', new Text(0), 'APNS team ID.', true)
->param('bundleId', '', new Text(0), 'APNS bundle ID.', true)
->param('sandbox', false, new Boolean(), 'Use APNS sandbox environment.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -1154,12 +1158,6 @@ 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'))),
]);
$audit = new Audit($dbForProject);
$resource = 'provider/' . $providerId;
$logs = $audit->getLogsByResource($resource, $queries);
@@ -1270,8 +1268,8 @@ App::patch('/v1/messaging/providers/mailgun/:providerId')
->param('name', '', new Text(128), 'Provider name.', true)
->param('apiKey', '', new Text(0), 'Mailgun API Key.', true)
->param('domain', '', new Text(0), 'Mailgun Domain.', true)
->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('isEuRegion', null, new Nullable(new Boolean()), 'Set as EU region.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('fromName', '', new Text(128), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128), 'Name set in the reply to field for the mail. Default value is sender name.', true)
@@ -1381,7 +1379,7 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('apiKey', '', new Text(0), 'Sendgrid API key.', true)
->param('fromName', '', new Text(128), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
@@ -1479,7 +1477,7 @@ App::patch('/v1/messaging/providers/resend/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('apiKey', '', new Text(0), 'Resend API key.', true)
->param('fromName', '', new Text(128), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
@@ -1579,6 +1577,7 @@ App::patch('/v1/messaging/providers/smtp/:providerId')
since: '1.8.0',
replaceWith: 'messaging.updateSMTPProvider',
),
public: false,
),
new Method(
namespace: 'messaging',
@@ -1597,17 +1596,17 @@ App::patch('/v1/messaging/providers/smtp/:providerId')
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('host', '', new Text(0), 'SMTP hosts. Either a single hostname or multiple semicolon-delimited hostnames. You can also specify a different port for each host such as `smtp1.example.com:25;smtp2.example.com`. You can also specify encryption type, for example: `tls://smtp1.example.com:587;ssl://smtp2.example.com:465"`. Hosts will be tried in order.', true)
->param('port', null, new Range(1, 65535), 'SMTP port.', true)
->param('port', null, new Nullable(new Range(1, 65535)), 'SMTP port.', true)
->param('username', '', new Text(0), 'Authentication username.', true)
->param('password', '', new Text(0), 'Authentication password.', true)
->param('encryption', '', new WhiteList(['none', 'ssl', 'tls']), 'Encryption type. Can be \'ssl\' or \'tls\'', true)
->param('autoTLS', null, new Boolean(), 'Enable SMTP AutoTLS feature.', true)
->param('autoTLS', null, new Nullable(new Boolean()), 'Enable SMTP AutoTLS feature.', true)
->param('mailer', '', new Text(0), 'The value to use for the X-Mailer header.', true)
->param('fromName', '', new Text(128), 'Sender Name.', true)
->param('fromEmail', '', new Email(), 'Sender email address.', true)
->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name.', true)
->param('replyToEmail', '', new Text(128), 'Email set in the Reply To field for the mail. Default value is Sender Email.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -1725,7 +1724,7 @@ App::patch('/v1/messaging/providers/msg91/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('templateId', '', new Text(0), 'Msg91 template ID.', true)
->param('senderId', '', new Text(0), 'Msg91 sender ID.', true)
->param('authKey', '', new Text(0), 'Msg91 auth key.', true)
@@ -1812,7 +1811,7 @@ App::patch('/v1/messaging/providers/telesign/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('customerId', '', new Text(0), 'Telesign customer ID.', true)
->param('apiKey', '', new Text(0), 'Telesign API key.', true)
->param('from', '', new Text(256), 'Sender number.', true)
@@ -1901,7 +1900,7 @@ App::patch('/v1/messaging/providers/textmagic/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('username', '', new Text(0), 'Textmagic username.', true)
->param('apiKey', '', new Text(0), 'Textmagic apiKey.', true)
->param('from', '', new Text(256), 'Sender number.', true)
@@ -1990,7 +1989,7 @@ App::patch('/v1/messaging/providers/twilio/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('accountSid', '', new Text(0), 'Twilio account secret ID.', true)
->param('authToken', '', new Text(0), 'Twilio authentication token.', true)
->param('from', '', new Text(256), 'Sender number.', true)
@@ -2079,7 +2078,7 @@ App::patch('/v1/messaging/providers/vonage/:providerId')
))
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('apiKey', '', new Text(0), 'Vonage API key.', true)
->param('apiSecret', '', new Text(0), 'Vonage API secret.', true)
->param('from', '', new Text(256), 'Sender number.', true)
@@ -2170,6 +2169,7 @@ App::patch('/v1/messaging/providers/fcm/:providerId')
since: '1.8.0',
replaceWith: 'messaging.updateFCMProvider',
),
public: false,
),
new Method(
namespace: 'messaging',
@@ -2187,8 +2187,8 @@ App::patch('/v1/messaging/providers/fcm/:providerId')
])
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('serviceAccountJSON', null, new JSON(), 'FCM service account JSON.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('serviceAccountJSON', null, new Nullable(new JSON()), 'FCM service account JSON.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -2265,6 +2265,7 @@ App::patch('/v1/messaging/providers/apns/:providerId')
since: '1.8.0',
replaceWith: 'messaging.updateAPNSProvider',
),
public: false,
),
new Method(
namespace: 'messaging',
@@ -2282,12 +2283,12 @@ App::patch('/v1/messaging/providers/apns/:providerId')
])
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Set as enabled.', true)
->param('authKey', '', new Text(0), 'APNS authentication key.', true)
->param('authKeyId', '', new Text(0), 'APNS authentication key ID.', true)
->param('teamId', '', new Text(0), 'APNS team ID.', true)
->param('bundleId', '', new Text(0), 'APNS bundle ID.', true)
->param('sandbox', null, new Boolean(), 'Use APNS sandbox environment.', true)
->param('sandbox', null, new Nullable(new Boolean()), 'Use APNS sandbox environment.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -2561,12 +2562,6 @@ 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'))),
]);
$audit = new Audit($dbForProject);
$resource = 'topic/' . $topicId;
$logs = $audit->getLogsByResource($resource, $queries);
@@ -2676,8 +2671,8 @@ App::patch('/v1/messaging/topics/:topicId')
]
))
->param('topicId', '', new UID(), 'Topic ID.')
->param('name', null, new Text(128), 'Topic Name.', true)
->param('subscribe', null, new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with subscribe permission. By default all users are granted with any subscribe permission. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true)
->param('name', null, new Nullable(new Text(128)), 'Topic Name.', true)
->param('subscribe', null, new Nullable(new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE)), 'An array of role strings with subscribe permission. By default all users are granted with any subscribe permission. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
@@ -2984,12 +2979,6 @@ 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'))),
]);
$audit = new Audit($dbForProject);
$resource = 'subscriber/' . $subscriberId;
$logs = $audit->getLogsByResource($resource, $queries);
@@ -3190,7 +3179,7 @@ App::post('/v1/messaging/messages/email')
->param('attachments', [], new ArrayList(new CompoundUID()), 'Array of compound ID strings of bucket IDs and file IDs to be attached to the email. They should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('draft', false, new Boolean(), 'Is message a draft', true)
->param('html', false, new Boolean(), 'Is content of type HTML', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
@@ -3342,6 +3331,7 @@ App::post('/v1/messaging/messages/sms')
since: '1.8.0',
replaceWith: 'messaging.createSMS',
),
public: false,
),
new Method(
namespace: 'messaging',
@@ -3363,7 +3353,7 @@ App::post('/v1/messaging/messages/sms')
->param('users', [], new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', [], new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('draft', false, new Boolean(), 'Is message a draft', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
@@ -3486,7 +3476,7 @@ App::post('/v1/messaging/messages/push')
->param('topics', [], new ArrayList(new UID()), 'List of Topic IDs.', true)
->param('users', [], new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', [], new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('data', null, new JSON(), 'Additional key-value pair data for push notification.', true)
->param('data', null, new Nullable(new JSON()), 'Additional key-value pair data for push notification.', true)
->param('action', '', new Text(256), 'Action for push notification.', true)
->param('image', '', new CompoundUID(), 'Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('icon', '', new Text(256), 'Icon for push notification. Available only for Android and Web Platform.', true)
@@ -3495,7 +3485,7 @@ App::post('/v1/messaging/messages/push')
->param('tag', '', new Text(256), 'Tag for push notification. Available only for Android Platform.', true)
->param('badge', -1, new Integer(), 'Badge for push notification. Available only for iOS Platform.', true)
->param('draft', false, new Boolean(), 'Is message a draft', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('contentAvailable', false, new Boolean(), 'If set to true, the notification will be delivered in the background. Available only for iOS Platform.', true)
->param('critical', false, new Boolean(), 'If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.', true)
->param('priority', 'high', new WhiteList(['normal', 'high']), 'Set the notification priority. "normal" will consider device state and may not deliver notifications immediately. "high" will always attempt to immediately deliver the notification.', true)
@@ -3505,7 +3495,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;
@@ -3561,7 +3552,6 @@ 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';
$scheduleTime = $currentScheduledAt ?? $scheduledAt;
@@ -3582,7 +3572,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' => "{$platform['endpoint']}/storage/buckets/{$bucket->getId()}/files/{$file->getId()}/push?project={$project->getId()}&jwt={$jwt}",
];
}
@@ -3784,12 +3774,6 @@ 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'))),
]);
$audit = new Audit($dbForProject);
$resource = 'message/' . $messageId;
$logs = $audit->getLogsByResource($resource, $queries);
@@ -3981,17 +3965,17 @@ App::patch('/v1/messaging/messages/email/:messageId')
]
))
->param('messageId', '', new UID(), 'Message ID.')
->param('topics', null, new ArrayList(new UID()), 'List of Topic IDs.', true)
->param('users', null, new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', null, new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('subject', null, new Text(998), 'Email Subject.', true)
->param('content', null, new Text(64230), 'Email Content.', true)
->param('draft', null, new Boolean(), 'Is message a draft', true)
->param('html', null, new Boolean(), 'Is content of type HTML', true)
->param('cc', null, new ArrayList(new UID()), 'Array of target IDs to be added as CC.', true)
->param('bcc', null, new ArrayList(new UID()), 'Array of target IDs to be added as BCC.', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('attachments', null, new ArrayList(new CompoundUID()), 'Array of compound ID strings of bucket IDs and file IDs to be attached to the email. They should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('topics', null, new Nullable(new ArrayList(new UID())), 'List of Topic IDs.', true)
->param('users', null, new Nullable(new ArrayList(new UID())), 'List of User IDs.', true)
->param('targets', null, new Nullable(new ArrayList(new UID())), 'List of Targets IDs.', true)
->param('subject', null, new Nullable(new Text(998)), 'Email Subject.', true)
->param('content', null, new Nullable(new Text(64230)), 'Email Content.', true)
->param('draft', null, new Nullable(new Boolean()), 'Is message a draft', true)
->param('html', null, new Nullable(new Boolean()), 'Is content of type HTML', true)
->param('cc', null, new Nullable(new ArrayList(new UID())), 'Array of target IDs to be added as CC.', true)
->param('bcc', null, new Nullable(new ArrayList(new UID())), 'Array of target IDs to be added as BCC.', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('attachments', null, new Nullable(new ArrayList(new CompoundUID())), 'Array of compound ID strings of bucket IDs and file IDs to be attached to the email. They should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
@@ -4191,6 +4175,7 @@ App::patch('/v1/messaging/messages/sms/:messageId')
since: '1.8.0',
replaceWith: 'messaging.updateSMS',
),
public: false,
),
new Method(
namespace: 'messaging',
@@ -4207,12 +4192,12 @@ App::patch('/v1/messaging/messages/sms/:messageId')
)
])
->param('messageId', '', new UID(), 'Message ID.')
->param('topics', null, new ArrayList(new UID()), 'List of Topic IDs.', true)
->param('users', null, new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', null, new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('content', null, new Text(64230), 'Email Content.', true)
->param('draft', null, new Boolean(), 'Is message a draft', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('topics', null, new Nullable(new ArrayList(new UID())), 'List of Topic IDs.', true)
->param('users', null, new Nullable(new ArrayList(new UID())), 'List of User IDs.', true)
->param('targets', null, new Nullable(new ArrayList(new UID())), 'List of Targets IDs.', true)
->param('content', null, new Nullable(new Text(64230)), 'Email Content.', true)
->param('draft', null, new Nullable(new Boolean()), 'Is message a draft', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
@@ -4369,31 +4354,32 @@ App::patch('/v1/messaging/messages/push/:messageId')
]
))
->param('messageId', '', new UID(), 'Message ID.')
->param('topics', null, new ArrayList(new UID()), 'List of Topic IDs.', true)
->param('users', null, new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', null, new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('title', null, new Text(256), 'Title for push notification.', true)
->param('body', null, new Text(64230), 'Body for push notification.', true)
->param('data', null, new JSON(), 'Additional Data for push notification.', true)
->param('action', null, new Text(256), 'Action for push notification.', true)
->param('image', null, new CompoundUID(), 'Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('icon', null, new Text(256), 'Icon for push notification. Available only for Android and Web platforms.', true)
->param('sound', null, new Text(256), 'Sound for push notification. Available only for Android and iOS platforms.', true)
->param('color', null, new Text(256), 'Color for push notification. Available only for Android platforms.', true)
->param('tag', null, new Text(256), 'Tag for push notification. Available only for Android platforms.', true)
->param('badge', null, new Integer(), 'Badge for push notification. Available only for iOS platforms.', true)
->param('draft', null, new Boolean(), 'Is message a draft', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('contentAvailable', null, new Boolean(), 'If set to true, the notification will be delivered in the background. Available only for iOS Platform.', true)
->param('critical', null, new Boolean(), 'If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.', true)
->param('priority', null, new WhiteList(['normal', 'high']), 'Set the notification priority. "normal" will consider device battery state and may send notifications later. "high" will always attempt to immediately deliver the notification.', true)
->param('topics', null, new Nullable(new ArrayList(new UID())), 'List of Topic IDs.', true)
->param('users', null, new Nullable(new ArrayList(new UID())), 'List of User IDs.', true)
->param('targets', null, new Nullable(new ArrayList(new UID())), 'List of Targets IDs.', true)
->param('title', null, new Nullable(new Text(256)), 'Title for push notification.', true)
->param('body', null, new Nullable(new Text(64230)), 'Body for push notification.', true)
->param('data', null, new Nullable(new JSON()), 'Additional Data for push notification.', true)
->param('action', null, new Nullable(new Text(256)), 'Action for push notification.', true)
->param('image', null, new Nullable(new CompoundUID()), 'Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('icon', null, new Nullable(new Text(256)), 'Icon for push notification. Available only for Android and Web platforms.', true)
->param('sound', null, new Nullable(new Text(256)), 'Sound for push notification. Available only for Android and iOS platforms.', true)
->param('color', null, new Nullable(new Text(256)), 'Color for push notification. Available only for Android platforms.', true)
->param('tag', null, new Nullable(new Text(256)), 'Tag for push notification. Available only for Android platforms.', true)
->param('badge', null, new Nullable(new Integer()), 'Badge for push notification. Available only for iOS platforms.', true)
->param('draft', null, new Nullable(new Boolean()), 'Is message a draft', true)
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('contentAvailable', null, new Nullable(new Boolean()), 'If set to true, the notification will be delivered in the background. Available only for iOS Platform.', true)
->param('critical', null, new Nullable(new Boolean()), 'If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.', true)
->param('priority', null, new Nullable(new WhiteList(['normal', 'high'])), 'Set the notification priority. "normal" will consider device battery state and may send notifications later. "high" will always attempt to immediately deliver the notification.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
->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()) {
@@ -4561,9 +4547,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');
@@ -4582,7 +4565,7 @@ App::patch('/v1/messaging/messages/push/:messageId')
$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' => "{$platform['endpoint']}/storage/buckets/{$bucket->getId()}/files/{$file->getId()}/push?project={$project->getId()}&jwt={$jwt}",
];
}
+4 -4
View File
@@ -468,7 +468,6 @@ App::post('/v1/migrations/csv/exports')
]
))
->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database to export.')
->param('bucketId', '', new UID(), 'Storage bucket unique ID where the exported CSV will be stored.')
->param('filename', '', new Text(255), 'The name of the file to be created for the export, excluding the .csv extension.')
->param('columns', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of attributes to export. If empty, all attributes will be exported. You can use the `*` wildcard to export all attributes from the collection.', true)
->param('queries', [], new ArrayList(new Text(0)), 'Array of query strings generated using the Query class provided by the SDK to filter documents to export. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
@@ -480,12 +479,12 @@ App::post('/v1/migrations/csv/exports')
->inject('user')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (
string $resourceId,
string $bucketId,
string $filename,
array $columns,
array $queries,
@@ -497,6 +496,7 @@ App::post('/v1/migrations/csv/exports')
Document $user,
Response $response,
Database $dbForProject,
Database $dbForPlatform,
Document $project,
Event $queueForEvents,
Migration $queueForMigrations
@@ -507,7 +507,7 @@ App::post('/v1/migrations/csv/exports')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'default'));
if ($bucket->isEmpty()) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
@@ -553,7 +553,7 @@ App::post('/v1/migrations/csv/exports')
'resourceData' => '{}',
'errors' => [],
'options' => [
'bucketId' => $bucketId,
'bucketId' => 'default', // Always use internal bucket
'filename' => $filename,
'columns' => $columns,
'queries' => $queries,
+3 -2
View File
@@ -18,6 +18,7 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Datetime as DateTimeValidator;
use Utopia\Database\Validator\UID;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@@ -526,8 +527,8 @@ App::put('/v1/project/variables/:variableId')
))
->param('variableId', '', new UID(), 'Variable unique ID.', false)
->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false)
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', true)
->param('secret', null, new Boolean(), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true)
->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
->inject('project')
->inject('response')
->inject('dbForProject')
+16 -9
View File
@@ -1,7 +1,6 @@
<?php
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\MockNumber;
use Appwrite\Event\Delete;
use Appwrite\Event\Mail;
@@ -46,6 +45,7 @@ use Utopia\Validator\Boolean;
use Utopia\Validator\Hostname;
use Utopia\Validator\Integer;
use Utopia\Validator\Multiple;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
@@ -118,7 +118,7 @@ App::post('/v1/projects')
'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT,
'passwordHistory' => 0,
'passwordDictionary' => false,
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG,
'duration' => TOKEN_EXPIRATION_LOGIN_LONG,
'personalDataCheck' => false,
'mockNumbers' => [],
'sessionAlerts' => false,
@@ -563,6 +563,7 @@ App::patch('/v1/projects/:projectId/api')
since: '1.8.0',
replaceWith: 'projects.updateAPIStatus',
),
public: false,
),
new Method(
namespace: 'projects',
@@ -620,6 +621,7 @@ App::patch('/v1/projects/:projectId/api/all')
since: '1.8.0',
replaceWith: 'projects.updateAPIStatusAll',
),
public: false,
),
new Method(
namespace: 'projects',
@@ -678,9 +680,9 @@ App::patch('/v1/projects/:projectId/oauth2')
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'Provider Name')
->param('appId', null, new Text(256), 'Provider app ID. Max length: 256 chars.', true)
->param('secret', null, new text(512), 'Provider secret key. Max length: 512 chars.', true)
->param('enabled', null, new Boolean(), 'Provider status. Set to \'false\' to disable new session creation.', true)
->param('appId', null, new Nullable(new Text(256)), 'Provider app ID. Max length: 256 chars.', true)
->param('secret', null, new Nullable(new text(512)), 'Provider secret key. Max length: 512 chars.', true)
->param('enabled', null, new Nullable(new Boolean()), 'Provider status. Set to \'false\' to disable new session creation.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $provider, ?string $appId, ?string $secret, ?bool $enabled, Response $response, Database $dbForPlatform) {
@@ -1476,8 +1478,8 @@ App::post('/v1/projects/:projectId/keys')
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('expire', null, new DatetimeValidator(), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
@@ -1615,8 +1617,8 @@ App::put('/v1/projects/:projectId/keys/:keyId')
->param('projectId', '', new UID(), 'Project unique ID.')
->param('keyId', '', new UID(), 'Key unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('expire', null, new DatetimeValidator(), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
@@ -2024,6 +2026,7 @@ App::patch('/v1/projects/:projectId/smtp')
since: '1.8.0',
replaceWith: 'projects.updateSMTP',
),
public: false,
),
new Method(
namespace: 'projects',
@@ -2140,6 +2143,7 @@ App::post('/v1/projects/:projectId/smtp/tests')
since: '1.8.0',
replaceWith: 'projects.createSMTPTest',
),
public: false,
),
new Method(
namespace: 'projects',
@@ -2234,6 +2238,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 +2405,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 +2530,7 @@ App::delete('/v1/projects/:projectId/templates/sms/:type/:locale')
since: '1.8.0',
replaceWith: 'projects.deleteSMSTemplate',
),
public: false,
),
new Method(
namespace: 'projects',
+57 -36
View File
@@ -2,7 +2,6 @@
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Auth;
use Appwrite\ClamAV\Network;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
@@ -13,6 +12,7 @@ use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries\Buckets;
use Appwrite\Utopia\Database\Validator\Queries\Files;
@@ -50,6 +50,7 @@ use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\HexColor;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@@ -77,7 +78,7 @@ App::post('/v1/storage/buckets')
))
->param('bucketId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('name', '', new Text(128), 'Bucket name')
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE)), 'An array of permission strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('fileSecurity', false, new Boolean(true), 'Enables configuring permissions for individual file. A user needs one of file or bucket level permissions to access a file. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true)
->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan'])
@@ -85,10 +86,11 @@ App::post('/v1/storage/buckets')
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD], true), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
->param('transformations', true, new Boolean(true), 'Are image transformations enabled?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $queueForEvents) {
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, bool $transformations, Response $response, Database $dbForProject, Event $queueForEvents) {
$bucketId = $bucketId === 'unique()' ? ID::unique() : $bucketId;
@@ -141,6 +143,7 @@ App::post('/v1/storage/buckets')
'compression' => $compression,
'encryption' => $encryption,
'antivirus' => $antivirus,
'transformations' => $transformations,
'search' => implode(' ', [$bucketId, $name]),
]));
@@ -290,7 +293,7 @@ App::put('/v1/storage/buckets/:bucketId')
))
->param('bucketId', '', new UID(), 'Bucket unique ID.')
->param('name', null, new Text(128), 'Bucket name', false)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE)), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('fileSecurity', false, new Boolean(true), 'Enables configuring permissions for individual file. A user needs one of file or bucket level permissions to access a file. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true)
->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan'])
@@ -298,10 +301,11 @@ App::put('/v1/storage/buckets/:bucketId')
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD], true), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
->param('transformations', true, new Boolean(true), 'Are image transformations enabled?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $queueForEvents) {
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, bool $transformations, Response $response, Database $dbForProject, Event $queueForEvents) {
$bucket = $dbForProject->getDocument('buckets', $bucketId);
if ($bucket->isEmpty()) {
@@ -315,6 +319,7 @@ App::put('/v1/storage/buckets/:bucketId')
$encryption ??= $bucket->getAttribute('encryption', true);
$antivirus ??= $bucket->getAttribute('antivirus', true);
$compression ??= $bucket->getAttribute('compression', Compression::NONE);
$transformations ??= $bucket->getAttribute('transformations', true);
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions);
@@ -328,7 +333,8 @@ App::put('/v1/storage/buckets/:bucketId')
->setAttribute('enabled', $enabled)
->setAttribute('encryption', $encryption)
->setAttribute('compression', $compression)
->setAttribute('antivirus', $antivirus));
->setAttribute('antivirus', $antivirus)
->setAttribute('transformations', $transformations));
$dbForProject->updateCollection('bucket_' . $bucket->getSequence(), $permissions, $fileSecurity);
@@ -418,7 +424,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).')
->param('fileId', '', new CustomId(), 'File ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('file', [], new File(), 'Binary file. Appwrite SDKs provide helpers to handle file input. [Learn about file input](https://appwrite.io/docs/products/storage/upload-download#input-file).', skipValidation: true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permission strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE])), 'An array of permission strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->inject('request')
->inject('response')
->inject('dbForProject')
@@ -431,8 +437,8 @@ App::post('/v1/storage/buckets/:bucketId/files')
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -464,7 +470,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
// Users can only manage their own roles, API keys and Admin users can manage any
$roles = Authorization::getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
if (!User::isApp($roles) && !User::isPrivileged($roles)) {
foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) {
$permission = Permission::parse($permission);
@@ -675,7 +681,13 @@ App::post('/v1/storage/buckets/:bucketId/files')
'metadata' => $metadata,
]);
$file = $dbForProject->createDocument('bucket_' . $bucket->getSequence(), $doc);
try {
$file = $dbForProject->createDocument('bucket_' . $bucket->getSequence(), $doc);
} catch (DuplicateException) {
throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS);
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
} else {
$file = $file
->setAttribute('$permissions', $permissions)
@@ -725,6 +737,8 @@ App::post('/v1/storage/buckets/:bucketId/files')
try {
$file = $dbForProject->createDocument('bucket_' . $bucket->getSequence(), $doc);
} catch (DuplicateException) {
throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS);
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
@@ -793,8 +807,8 @@ App::get('/v1/storage/buckets/:bucketId/files')
->action(function (string $bucketId, array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject, string $mode) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -894,8 +908,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId')
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, string $mode) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -976,13 +990,17 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
/* @type Document $bucket */
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
if (!$bucket->getAttribute('transformations', true) && !$isAPIKey && !$isPrivilegedUser) {
throw new Exception(Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED);
}
$isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_READ);
@@ -1117,7 +1135,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'];
//Do not update transformedAt if it's a console user
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
if (!User::isPrivileged(Authorization::getRoles())) {
$transformedAt = $file->getAttribute('transformedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
$file->setAttribute('transformedAt', DateTime::now());
@@ -1168,8 +1186,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
/* @type Document $bucket */
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -1329,8 +1347,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
/* @type Document $bucket */
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -1477,12 +1495,11 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
->inject('response')
->inject('request')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('mode')
->inject('deviceForFiles')
->action(function (string $bucketId, string $fileId, string $jwt, Response $response, Request $request, Database $dbForProject, Document $project, string $mode, Device $deviceForFiles) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
->action(function (string $bucketId, string $fileId, string $jwt, Response $response, Request $request, Database $dbForProject, Database $dbForPlatform, Document $project, string $mode, Device $deviceForFiles) {
$decoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
try {
@@ -1499,15 +1516,19 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isInternal = $decoded['internal'] ?? false;
$disposition = $decoded['disposition'] ?? 'inline';
$dbForProject = $isInternal ? $dbForPlatform : $dbForProject;
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
@@ -1553,7 +1574,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
->setContentType($contentType)
->addHeader('Content-Security-Policy', 'script-src none;')
->addHeader('X-Content-Type-Options', 'nosniff')
->addHeader('Content-Disposition', 'inline; filename="' . $file->getAttribute('name', '') . '"')
->addHeader('Content-Disposition', $disposition . '; filename="' . $file->getAttribute('name', '') . '"')
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
->addHeader('X-Peak', \memory_get_peak_usage());
@@ -1645,8 +1666,8 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
))
->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).')
->param('fileId', '', new UID(), 'File unique ID.')
->param('name', null, new Text(255), 'Name of the file', true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permission string. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('name', null, new Nullable(new Text(255)), 'Name of the file', true)
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE])), 'An array of permission string. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->inject('response')
->inject('dbForProject')
->inject('user')
@@ -1656,8 +1677,8 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -1686,7 +1707,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
// Users can only manage their own roles, API keys and Admin users can manage any
$roles = Authorization::getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles) && !\is_null($permissions)) {
if (!User::isApp($roles) && !User::isPrivileged($roles) && !\is_null($permissions)) {
foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) {
$permission = Permission::parse($permission);
@@ -1770,8 +1791,8 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Device $deviceForFiles, Delete $queueForDeletes) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAPIKey = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
+94 -74
View File
@@ -1,6 +1,5 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
@@ -10,7 +9,7 @@ use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Email as EmailValidator;
use Appwrite\Network\Validator\Redirect;
use Appwrite\Platform\Workers\Deletes;
use Appwrite\SDK\AuthType;
@@ -18,6 +17,7 @@ use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Template\Template;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries\Memberships;
use Appwrite\Utopia\Database\Validator\Queries\Teams;
@@ -28,6 +28,9 @@ use MaxMind\Db\Reader;
use Utopia\Abuse\Abuse;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Auth\Proofs\Password;
use Utopia\Auth\Proofs\Token;
use Utopia\Auth\Store;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@@ -48,13 +51,13 @@ use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Emails\Email;
use Utopia\Locale\Locale;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
App::post('/v1/teams')
@@ -86,8 +89,8 @@ App::post('/v1/teams')
->inject('queueForEvents')
->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$isAppUser = User::isApp(Authorization::getRoles());
$teamId = $teamId == 'unique()' ? ID::unique() : $teamId;
@@ -175,6 +178,7 @@ App::get('/v1/teams')
->inject('dbForProject')
->action(function (array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@@ -468,21 +472,20 @@ App::post('/v1/teams/:teamId/memberships')
))
->label('abuse-limit', 10)
->param('teamId', '', new UID(), 'Team ID.')
->param('email', '', new Email(), 'Email of the new team member.', true)
->param('email', '', new EmailValidator(), 'Email of the new team member.', true)
->param('userId', '', new UID(), 'ID of the user to be added to a team.', true)
->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('roles', [], function (Document $project) {
if ($project->getId() === 'console') {
;
$roles = array_keys(Config::getParam('roles', []));
array_filter($roles, function ($role) {
return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]);
$roles = array_filter($roles, function ($role) {
return !in_array($role, [User::ROLE_APPS, User::ROLE_GUESTS, User::ROLE_USERS]);
});
return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE);
}
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')
@@ -495,9 +498,11 @@ App::post('/v1/teams/:teamId/memberships')
->inject('timelimit')
->inject('queueForStatsUsage')
->inject('plan')
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan) {
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
->inject('proofForPassword')
->inject('proofForToken')
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Password $proofForPassword, Token $proofForToken) {
$isAppUser = User::isApp(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$url = htmlentities($url);
if (empty($url)) {
@@ -568,37 +573,53 @@ App::post('/v1/teams/:teamId/memberships')
try {
$userId = ID::unique();
$invitee = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$permissions' => [
Permission::read(Role::any()),
Permission::read(Role::user($userId)),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
],
'email' => empty($email) ? null : $email,
'phone' => empty($phone) ? null : $phone,
'emailVerification' => false,
'status' => true,
// TODO: Set password empty?
'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),
'hash' => Auth::DEFAULT_ALGO,
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
/**
* Set the password update time to 0 for users created using
* team invite and OAuth to allow password updates without an
* old password
*/
'passwordUpdate' => null,
'registration' => DateTime::now(),
'reset' => false,
'name' => $name,
'prefs' => new \stdClass(),
'sessions' => null,
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $name]),
])));
$hash = $proofForPassword->hash($proofForPassword->generate());
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
}
$userId = ID::unique();
$userDocument = new Document([
'$id' => $userId,
'$permissions' => [
Permission::read(Role::any()),
Permission::read(Role::user($userId)),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
],
'email' => empty($email) ? null : $email,
'phone' => empty($phone) ? null : $phone,
'emailVerification' => false,
'status' => true,
// TODO: Set password empty?
'password' => $hash,
'hash' => $proofForPassword->getHash()->getName(),
'hashOptions' => $proofForPassword->getHash()->getOptions(),
/**
* Set the password update time to 0 for users created using
* team invite and OAuth to allow password updates without an
* old password
*/
'passwordUpdate' => null,
'registration' => DateTime::now(),
'reset' => false,
'name' => $name,
'prefs' => new \stdClass(),
'sessions' => null,
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $name]),
'emailCanonical' => $emailCanonical?->getCanonical(),
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
'emailIsCorporate' => $emailCanonical?->isCorporate(),
'emailIsDisposable' => $emailCanonical?->isDisposable(),
'emailIsFree' => $emailCanonical?->isFree(),
]);
try {
$invitee = Authorization::skip(fn () => $dbForProject->createDocument('users', $userDocument));
} catch (Duplicate $th) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
@@ -615,7 +636,7 @@ App::post('/v1/teams/:teamId/memberships')
Query::equal('teamInternalId', [$team->getSequence()]),
]);
$secret = Auth::tokenGenerator();
$secret = $proofForToken->generate();
if ($membership->isEmpty()) {
$membershipId = ID::unique();
$membership = new Document([
@@ -635,7 +656,7 @@ App::post('/v1/teams/:teamId/memberships')
'invited' => DateTime::now(),
'joined' => ($isPrivilegedUser || $isAppUser) ? DateTime::now() : null,
'confirm' => ($isPrivilegedUser || $isAppUser),
'secret' => Auth::hash($secret),
'secret' => $proofForToken->hash($secret),
'search' => implode(' ', [$membershipId, $invitee->getId()])
]);
@@ -646,9 +667,8 @@ App::post('/v1/teams/:teamId/memberships')
if ($isPrivilegedUser || $isAppUser) {
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
}
} elseif ($membership->getAttribute('confirm') === false) {
$membership->setAttribute('secret', Auth::hash($secret));
$membership->setAttribute('secret', $proofForToken->hash($secret));
$membership->setAttribute('invited', DateTime::now());
if ($isPrivilegedUser || $isAppUser) {
@@ -751,7 +771,6 @@ App::post('/v1/teams/:teamId/memberships')
->setName($invitee->getAttribute('name', ''))
->setVariables($emailVariables)
->trigger();
} elseif (!empty($phone)) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
@@ -915,8 +934,8 @@ App::get('/v1/teams/:teamId/memberships')
];
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
$isPrivilegedUser = User::isPrivileged($roles);
$isAppUser = User::isApp($roles);
$membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) {
return $privacy || $isPrivilegedUser || $isAppUser;
@@ -1006,8 +1025,8 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
];
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
$isPrivilegedUser = User::isPrivileged($roles);
$isAppUser = User::isApp($roles);
$membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) {
return $privacy || $isPrivilegedUser || $isAppUser;
@@ -1072,8 +1091,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
->param('roles', [], function (Document $project) {
if ($project->getId() === 'console') {
$roles = array_keys(Config::getParam('roles', []));
array_filter($roles, function ($role) {
return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]);
$roles = array_filter($roles, function ($role) {
return !in_array($role, [User::ROLE_APPS, User::ROLE_GUESTS, User::ROLE_USERS]);
});
return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE);
}
@@ -1102,8 +1121,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
throw new Exception(Exception::USER_NOT_FOUND);
}
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$isAppUser = User::isApp(Authorization::getRoles());
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');
if ($project->getId() === 'console') {
@@ -1188,7 +1207,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->inject('project')
->inject('geodb')
->inject('queueForEvents')
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents) {
->inject('store')
->inject('proofForToken')
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) {
$protocol = $request->getProtocol();
$membership = $dbForProject->getDocument('memberships', $membershipId);
@@ -1207,7 +1228,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
throw new Exception(Exception::TEAM_MEMBERSHIP_MISMATCH);
}
if (Auth::hash($secret) !== $membership->getAttribute('secret')) {
if (!$proofForToken->verify($secret, $membership->getAttribute('secret'))) {
throw new Exception(Exception::TEAM_INVALID_SECRET);
}
@@ -1241,9 +1262,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$authDuration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::addSeconds(new \DateTime(), $authDuration);
$secret = Auth::tokenGenerator();
$secret = $proofForToken->generate();
$session = new Document(array_merge([
'$id' => ID::unique(),
'$permissions' => [
@@ -1253,9 +1274,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
],
'userId' => $user->getId(),
'userInternalId' => $user->getSequence(),
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'provider' => SESSION_PROVIDER_EMAIL,
'providerUid' => $user->getAttribute('email'),
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'factors' => ['email'],
@@ -1267,14 +1288,19 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
Authorization::setRole(Role::user($userId)->toString());
$encoded = $store
->setProperty('id', $user->getId())
->setProperty('secret', $secret)
->encode();
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]));
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
}
$response
->addCookie(
name: Auth::$cookieName . '_legacy',
value: Auth::encodeSession($user->getId(), $secret),
name: $store->getKey() . '_legacy',
value: $encoded,
expire: (new \DateTime($expire))->getTimestamp(),
path: '/',
domain: Config::getParam('cookieDomain'),
@@ -1282,8 +1308,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
httponly: true
)
->addCookie(
name: Auth::$cookieName,
value: Auth::encodeSession($user->getId(), $secret),
name: $store->getKey(),
value: $encoded,
expire: (new \DateTime($expire))->getTimestamp(),
path: '/',
domain: Config::getParam('cookieDomain'),
@@ -1452,12 +1478,6 @@ 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'))),
]);
$audit = new Audit($dbForProject);
$resource = 'team/' . $team->getId();
$logs = $audit->getLogsByResource($resource, $queries);
+145 -94
View File
@@ -1,7 +1,6 @@
<?php
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Auth\MFA\Type;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Auth\Validator\Password;
@@ -16,7 +15,7 @@ use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Email as EmailValidator;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
@@ -32,6 +31,18 @@ use Appwrite\Utopia\Response;
use MaxMind\Db\Reader;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Auth\Hash;
use Utopia\Auth\Hashes\Argon2;
use Utopia\Auth\Hashes\Bcrypt;
use Utopia\Auth\Hashes\MD5;
use Utopia\Auth\Hashes\PHPass;
use Utopia\Auth\Hashes\Plaintext;
use Utopia\Auth\Hashes\Scrypt;
use Utopia\Auth\Hashes\ScryptModified;
use Utopia\Auth\Hashes\Sha;
use Utopia\Auth\Proofs\Password as ProofsPassword;
use Utopia\Auth\Proofs\Token;
use Utopia\Auth\Store;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@@ -49,21 +60,22 @@ use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Emails\Email;
use Utopia\Locale\Locale;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
/** TODO: Remove function when we move to using utopia/platform */
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
{
$plaintextPassword = $password;
$hashOptionsObject = (\is_string($hashOptions)) ? \json_decode($hashOptions, true) : $hashOptions; // Cast to JSON array
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
if (!empty($email)) {
@@ -97,7 +109,29 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
}
}
$password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null;
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
}
$hashedPassword = null;
$isHashed = !$hash instanceof Plaintext;
$defaultHash = new ProofsPassword();
if (!empty($password)) {
if (!$isHashed) { // Password was never hashed, hash it with the default hash
$hashedPassword = $defaultHash->hash($password);
$hash = $defaultHash->getHash();
} else {
$hashedPassword = $password;
}
} else {
// when password is not provided, plaintext was set as the default hash causing the issue
$hash = $defaultHash->getHash();
$isHashed = !$hash instanceof Plaintext;
}
$user = new Document([
'$id' => $userId,
'$permissions' => [
@@ -111,11 +145,11 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
'phoneVerification' => false,
'status' => true,
'labels' => [],
'password' => $password,
'passwordHistory' => is_null($password) || $passwordHistory === 0 ? [] : [$password],
'passwordUpdate' => (!empty($password)) ? DateTime::now() : null,
'hash' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO : $hash,
'hashOptions' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO_OPTIONS : $hashOptionsObject + ['type' => $hash],
'password' => $hashedPassword,
'passwordHistory' => is_null($hashedPassword) || $passwordHistory === 0 ? [] : [$hashedPassword],
'passwordUpdate' => (!empty($hashedPassword)) ? DateTime::now() : null,
'hash' => $hash->getName(),
'hashOptions' => $hash->getOptions(),
'registration' => DateTime::now(),
'reset' => false,
'name' => $name,
@@ -124,9 +158,14 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $phone, $name]),
'emailCanonical' => $emailCanonical?->getCanonical(),
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
'emailIsCorporate' => $emailCanonical?->isCorporate(),
'emailIsDisposable' => $emailCanonical?->isDisposable(),
'emailIsFree' => $emailCanonical?->isFree(),
]);
if ($hash === 'plaintext') {
if (!$isHashed && !empty($password)) {
$hooks->trigger('passwordValidator', [$dbForProject, $project, $plaintextPassword, &$user, true]);
}
@@ -208,8 +247,8 @@ App::post('/v1/users')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', null, new Email(), 'User email.', true)
->param('phone', null, new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('email', null, new Nullable(new EmailValidator()), 'User email.', true)
->param('phone', null, new Nullable(new Phone()), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'Plain text user password. Must be at least 8 chars.', true, ['project', 'passwordsDictionary'])
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -217,7 +256,9 @@ App::post('/v1/users')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks);
$plaintext = new Plaintext();
$user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($user, Response::MODEL_USER);
@@ -243,7 +284,7 @@ App::post('/v1/users/bcrypt')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Bcrypt.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -251,7 +292,10 @@ App::post('/v1/users/bcrypt')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$bcrypt = new Bcrypt();
$bcrypt->setCost(8); // Default cost
$user = createUser($bcrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -278,7 +322,7 @@ App::post('/v1/users/md5')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using MD5.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -286,7 +330,9 @@ App::post('/v1/users/md5')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$user = createUser('md5', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$md5 = new MD5();
$user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -313,7 +359,7 @@ App::post('/v1/users/argon2')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Argon2.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -321,7 +367,9 @@ App::post('/v1/users/argon2')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$argon2 = new Argon2();
$user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -348,7 +396,7 @@ App::post('/v1/users/sha')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using SHA.')
->param('passwordVersion', '', new WhiteList(['sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512']), "Optional SHA version used to hash password. Allowed values are: 'sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512'", true)
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
@@ -357,13 +405,12 @@ App::post('/v1/users/sha')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$options = '{}';
$sha = new Sha();
if (!empty($passwordVersion)) {
$options = '{"version":"' . $passwordVersion . '"}';
$sha->setVersion($passwordVersion);
}
$user = createUser('sha', $options, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser($sha, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -390,7 +437,7 @@ App::post('/v1/users/phpass')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or pass the string `ID.unique()`to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using PHPass.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -398,7 +445,9 @@ App::post('/v1/users/phpass')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$phpass = new PHPass();
$user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -425,7 +474,7 @@ App::post('/v1/users/scrypt')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Scrypt.')
->param('passwordSalt', '', new Text(128), 'Optional salt used to hash password.')
->param('passwordCpu', 8, new Integer(), 'Optional CPU cost used to hash password.')
@@ -438,15 +487,15 @@ App::post('/v1/users/scrypt')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$options = [
'salt' => $passwordSalt,
'costCpu' => $passwordCpu,
'costMemory' => $passwordMemory,
'costParallel' => $passwordParallel,
'length' => $passwordLength
];
$scrypt = new Scrypt();
$scrypt
->setSalt($passwordSalt)
->setCpuCost($passwordCpu)
->setMemoryCost($passwordMemory)
->setParallelCost($passwordParallel)
->setLength($passwordLength);
$user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser($scrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -473,7 +522,7 @@ App::post('/v1/users/scrypt-modified')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Scrypt Modified.')
->param('passwordSalt', '', new Text(128), 'Salt used to hash password.')
->param('passwordSaltSeparator', '', new Text(128), 'Salt separator used to hash password.')
@@ -484,7 +533,13 @@ App::post('/v1/users/scrypt-modified')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$scryptModified = new ScryptModified();
$scryptModified
->setSalt($passwordSalt)
->setSaltSeparator($passwordSaltSeparator)
->setSignerKey($passwordSignerKey);
$user = createUser($scryptModified, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -527,7 +582,7 @@ App::post('/v1/users/:userId/targets')
switch ($providerType) {
case 'email':
$validator = new Email();
$validator = new EmailValidator();
if (!$validator->isValid($identifier)) {
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
@@ -796,16 +851,12 @@ App::get('/v1/users/:userId/sessions')
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$sessions = $user->getAttribute('sessions', []);
foreach ($sessions as $key => $session) {
/** @var Document $session */
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
$session->setAttribute('countryName', $countryName);
$session->setAttribute('current', false);
$sessions[$key] = $session;
}
@@ -845,28 +896,22 @@ App::get('/v1/users/:userId/memberships')
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
if (!empty($search)) {
$queries[] = Query::search('search', $search);
}
// Set internal queries
$queries[] = Query::equal('userInternalId', [$user->getSequence()]);
$memberships = array_map(function ($membership) use ($dbForProject, $user) {
$team = $dbForProject->getDocument('teams', $membership->getAttribute('teamId'));
$membership
->setAttribute('teamName', $team->getAttribute('name'))
->setAttribute('userName', $user->getAttribute('name'))
->setAttribute('userEmail', $user->getAttribute('email'));
return $membership;
}, $dbForProject->find('memberships', $queries));
@@ -907,35 +952,22 @@ App::get('/v1/users/:userId/logs')
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Temp fix for logs
$queries[] = Query::or([
Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))),
Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))),
]);
$audit = new Audit($dbForProject);
$logs = $audit->getLogsByUser($user->getSequence(), $queries);
$output = [];
foreach ($logs as $i => &$log) {
$log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN';
$detector = new Detector($log['userAgent']);
$detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
$os = $detector->getOS();
$client = $detector->getClient();
$device = $detector->getDevice();
$output[$i] = new Document([
'event' => $log['event'],
'userId' => ID::custom($log['data']['userId']),
@@ -956,9 +988,7 @@ App::get('/v1/users/:userId/logs')
'deviceBrand' => $device['deviceBrand'],
'deviceModel' => $device['deviceModel']
]);
$record = $geodb->get($log['ip']);
if ($record) {
$output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--';
$output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
@@ -1002,15 +1032,12 @@ App::get('/v1/users/:userId/targets')
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$queries[] = Query::equal('userId', [$userId]);
/**
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
*/
@@ -1018,20 +1045,16 @@ App::get('/v1/users/:userId/targets')
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
});
$cursor = reset($cursor);
if ($cursor) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$targetId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('targets', $targetId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Target '{$targetId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
try {
@@ -1079,7 +1102,6 @@ App::get('/v1/users/identities')
if (!empty($search)) {
$queries[] = Query::search('search', $search);
}
/**
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
*/
@@ -1089,19 +1111,15 @@ App::get('/v1/users/identities')
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$identityId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('identities', $identityId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "User '{$identityId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
@@ -1341,12 +1359,17 @@ App::patch('/v1/users/:userId/password')
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
// Create Argon2 hasher with default settings
$hasher = new Argon2();
$newPassword = $hasher->hash($password);
$hash = ProofsPassword::createHash($user->getAttribute('hash'), $user->getAttribute('hashOptions'));
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
$history = $user->getAttribute('passwordHistory', []);
if ($historyLimit > 0) {
$validator = new PasswordHistory($history, $user->getAttribute('hash'), $user->getAttribute('hashOptions'));
$validator = new PasswordHistory($history, $hash);
if (!$validator->isValid($password)) {
throw new Exception(Exception::USER_PASSWORD_RECENTLY_USED);
}
@@ -1359,8 +1382,8 @@ App::patch('/v1/users/:userId/password')
->setAttribute('password', $newPassword)
->setAttribute('passwordHistory', $history)
->setAttribute('passwordUpdate', DateTime::now())
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS);
->setAttribute('hash', $hasher->getName())
->setAttribute('hashOptions', $hasher->getOptions());
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
@@ -1402,7 +1425,7 @@ App::patch('/v1/users/:userId/email')
]
))
->param('userId', '', new UID(), 'User ID.')
->param('email', '', new Email(allowEmpty: true), 'User email.')
->param('email', '', new EmailValidator(allowEmpty: true), 'User email.')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
@@ -1437,9 +1460,20 @@ App::patch('/v1/users/:userId/email')
$oldEmail = $user->getAttribute('email');
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
}
$user
->setAttribute('email', $email)
->setAttribute('emailVerification', false)
->setAttribute('emailCanonical', $emailCanonical?->getCanonical())
->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported())
->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate())
->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable())
->setAttribute('emailIsFree', $emailCanonical?->isFree())
;
try {
@@ -1700,7 +1734,7 @@ App::patch('/v1/users/:userId/targets/:targetId')
switch ($providerType) {
case 'email':
$validator = new Email();
$validator = new EmailValidator();
if (!$validator->isValid($identifier)) {
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
@@ -1779,6 +1813,7 @@ App::patch('/v1/users/:userId/mfa')
since: '1.8.0',
replaceWith: 'users.updateMFA',
),
public: false,
),
new Method(
namespace: 'users',
@@ -1838,6 +1873,7 @@ App::get('/v1/users/:userId/mfa/factors')
since: '1.8.0',
replaceWith: 'users.listMFAFactors',
),
public: false,
),
new Method(
namespace: 'users',
@@ -1896,6 +1932,7 @@ App::get('/v1/users/:userId/mfa/recovery-codes')
since: '1.8.0',
replaceWith: 'users.getMFARecoveryCodes',
),
public: false,
),
new Method(
namespace: 'users',
@@ -1960,6 +1997,7 @@ App::patch('/v1/users/:userId/mfa/recovery-codes')
since: '1.8.0',
replaceWith: 'users.createMFARecoveryCodes',
),
public: false,
),
new Method(
namespace: 'users',
@@ -2031,6 +2069,7 @@ App::put('/v1/users/:userId/mfa/recovery-codes')
since: '1.8.0',
replaceWith: 'users.updateMFARecoveryCodes',
),
public: false,
),
new Method(
namespace: 'users',
@@ -2043,7 +2082,8 @@ App::put('/v1/users/:userId/mfa/recovery-codes')
code: Response::STATUS_CODE_OK,
model: Response::MODEL_MFA_RECOVERY_CODES,
)
]
],
public: false,
)
])
->param('userId', '', new UID(), 'User ID.')
@@ -2102,6 +2142,7 @@ App::delete('/v1/users/:userId/mfa/authenticators/:type')
since: '1.8.0',
replaceWith: 'users.deleteMFAAuthenticator',
),
public: false,
),
new Method(
namespace: 'users',
@@ -2173,17 +2214,19 @@ App::post('/v1/users/:userId/sessions')
->inject('locale')
->inject('geodb')
->inject('queueForEvents')
->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents) {
->inject('store')
->inject('proofForToken')
->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION);
$secret = $proofForToken->generate();
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$session = new Document(array_merge(
@@ -2191,8 +2234,8 @@ App::post('/v1/users/:userId/sessions')
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getSequence(),
'provider' => Auth::SESSION_PROVIDER_SERVER,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'provider' => SESSION_PROVIDER_SERVER,
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'factors' => ['server'],
'ip' => $request->getIP(),
@@ -2216,8 +2259,13 @@ App::post('/v1/users/:userId/sessions')
$dbForProject->purgeCachedDocument('users', $user->getId());
$encoded = $store
->setProperty('id', $user->getId())
->setProperty('secret', $secret)
->encode();
$session
->setAttribute('secret', Auth::encodeSession($user->getId(), $secret))
->setAttribute('secret', $encoded)
->setAttribute('countryName', $countryName);
$queueForEvents
@@ -2252,7 +2300,7 @@ App::post('/v1/users/:userId/tokens')
))
->param('userId', '', new UID(), 'User ID.')
->param('length', 6, new Range(4, 128), 'Token length in characters. The default length is 6 characters', true)
->param('expire', Auth::TOKEN_EXPIRATION_GENERIC, new Range(60, Auth::TOKEN_EXPIRATION_LOGIN_LONG), 'Token expiration period in seconds. The default expiration is 15 minutes.', true)
->param('expire', TOKEN_EXPIRATION_GENERIC, new Range(60, TOKEN_EXPIRATION_LOGIN_LONG), 'Token expiration period in seconds. The default expiration is 15 minutes.', true)
->inject('request')
->inject('response')
->inject('dbForProject')
@@ -2264,15 +2312,17 @@ App::post('/v1/users/:userId/tokens')
throw new Exception(Exception::USER_NOT_FOUND);
}
$secret = Auth::tokenGenerator($length);
$proofForToken = new Token($length);
$proofForToken->setHash(new Sha());
$secret = $proofForToken->generate();
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $expire));
$token = new Document([
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getSequence(),
'type' => Auth::TOKEN_TYPE_GENERIC,
'secret' => Auth::hash($secret),
'type' => TOKEN_TYPE_GENERIC,
'secret' => $proofForToken->hash($secret),
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP()
@@ -2585,7 +2635,8 @@ App::post('/v1/users/:userId/jwts')
$session = \count($sessions) > 0 ? $sessions[\count($sessions) - 1] : new Document();
} else {
// Find by ID
foreach ($sessions as $loopSession) { /** @var Utopia\Database\Document $loopSession */
foreach ($sessions as $loopSession) {
/** @var Utopia\Database\Document $loopSession */
if ($loopSession->getId() == $sessionId) {
$session = $loopSession;
break;
+130 -27
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;
@@ -14,9 +13,12 @@ use Appwrite\Utopia\Database\Validator\Queries\Installations;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Appwrite\Vcs\Comment;
use Swoole\Coroutine\WaitGroup;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Adapters\Dotenv as ConfigDotenv;
use Utopia\Config\Config;
use Utopia\Config\Exceptions\Parse;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
@@ -28,7 +30,10 @@ use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Detector\Detection\Framework\Analog;
use Utopia\Detector\Detection\Framework\Angular;
use Utopia\Detector\Detection\Framework\Astro;
@@ -71,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, Build $queueForBuilds, callable $getProjectDB, array $platform) {
$errors = [];
foreach ($repositories as $repository) {
try {
@@ -127,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}";
@@ -169,7 +174,7 @@ $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, '');
@@ -179,7 +184,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
}
}
} 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()));
@@ -240,7 +245,7 @@ $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, '');
@@ -461,7 +466,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$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());
@@ -536,12 +541,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,
@@ -550,7 +555,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.');
@@ -579,10 +584,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);
@@ -609,7 +614,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",
@@ -963,6 +968,46 @@ App::post('/v1/vcs/github/installations/:installationId/detections')
throw new Exception(Exception::FUNCTION_RUNTIME_NOT_DETECTED);
}
}
$wg = new WaitGroup();
$envs = [];
foreach ($files as $file) {
if (!(\str_starts_with($file, '.env'))) {
continue;
}
$wg->add();
go(function () use ($github, $owner, $repositoryName, $providerRootDirectory, $file, $wg, &$envs) {
try {
$contentResponse = $github->getRepositoryContent($owner, $repositoryName, \rtrim($providerRootDirectory, '/') . '/' . $file);
$envFile = $contentResponse['content'] ?? '';
$configAdapter = new ConfigDotenv();
try {
$envObject = $configAdapter->parse($envFile);
foreach ($envObject as $envName => $envValue) {
$envs[$envName] = $envValue;
}
} catch (Parse $err) {
// Silence error, so rest of endpoint can return
}
} finally {
$wg->done();
}
});
}
$wg->wait();
$variables = [];
foreach ($envs as $key => $value) {
$variables[] = [
'name' => $key,
'value' => $value,
];
}
$output->setAttribute('variables', $variables);
$response->dynamic($output, $type === 'framework' ? Response::MODEL_DETECTION_FRAMEWORK : Response::MODEL_DETECTION_RUNTIME);
});
@@ -990,10 +1035,11 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
->param('installationId', '', new Text(256), 'Installation Id')
->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('gitHub')
->inject('response')
->inject('dbForPlatform')
->action(function (string $installationId, string $type, string $search, GitHub $github, Response $response, Database $dbForPlatform) {
->action(function (string $installationId, string $type, string $search, array $queries, GitHub $github, Response $response, Database $dbForPlatform) {
if (empty($search)) {
$search = "";
}
@@ -1009,11 +1055,20 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$page = 1;
$perPage = 4;
$queries = Query::parseQueries($queries);
$limitQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_LIMIT));
$offsetQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_OFFSET));
$limit = !empty($limitQuery) ? $limitQuery->getValue() : 4;
$offset = !empty($offsetQuery) ? $offsetQuery->getValue() : 0;
if ($offset % $limit !== 0) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'offset must be a multiple of the limit');
}
$page = ($offset / $limit) + 1;
$owner = $github->getOwnerName($providerInstallationId);
$repos = $github->searchRepositories($owner, $page, $perPage, $search);
['items' => $repos, 'total' => $total] = $github->searchRepositories($owner, $page, $limit, $search);
$repos = \array_map(function ($repo) use ($installation) {
$repo['id'] = \strval($repo['id'] ?? '');
@@ -1137,6 +1192,44 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
$repo['runtime'] = $runtimeWithVersion ?? '';
}
}
$wg = new WaitGroup();
$envs = [];
foreach ($files as $file) {
if (!(\str_starts_with($file, '.env'))) {
continue;
}
$wg->add();
go(function () use ($github, $repo, $file, $wg, &$envs) {
try {
$contentResponse = $github->getRepositoryContent($repo['organization'], $repo['name'], $file);
$envFile = $contentResponse['content'] ?? '';
$configAdapter = new ConfigDotenv();
try {
$envObject = $configAdapter->parse($envFile);
foreach ($envObject as $envName => $envValue) {
$envs[$envName] = $envValue;
}
} catch (Parse) {
// Silence error, so rest of endpoint can return
}
} finally {
$wg->done();
}
});
}
$wg->wait();
$repo['variables'] = [];
foreach ($envs as $key => $value) {
$repo['variables'][] = [
'name' => $key,
'value' => $value,
];
}
return $repo;
};
}, $repos));
@@ -1147,7 +1240,7 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
$response->dynamic(new Document([
$type === 'framework' ? 'frameworkProviderRepositories' : 'runtimeProviderRepositories' => $repos,
'total' => \count($repos),
'total' => $total,
]), ($type === 'framework') ? Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST : Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST);
});
@@ -1385,8 +1478,9 @@ App::post('/v1/vcs/github/events')
->inject('dbForPlatform')
->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, 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', '');
@@ -1429,7 +1523,7 @@ App::post('/v1/vcs/github/events')
// 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, $queueForBuilds, $getProjectDB, $platform);
}
} elseif ($event == $github::EVENT_INSTALLATION) {
if ($parsedPayload["action"] == "deleted") {
@@ -1485,7 +1579,7 @@ App::post('/v1/vcs/github/events')
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, $queueForBuilds, $getProjectDB, $platform);
} elseif ($parsedPayload["action"] == "closed") {
// Allowed external contributions cleanup
@@ -1689,20 +1783,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('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, Response $response, Document $project, Database $dbForPlatform, 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->getDocument('repositories', $repositoryId, [
$repository = Authorization::skip(fn () => $dbForPlatform->findOne('repositories', [
Query::equal('$id', [$repositoryId]),
Query::equal('projectInternalId', [$project->getSequence()])
]));
@@ -1742,8 +1837,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, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, true, $dbForPlatform, $queueForBuilds, $getProjectDB, $platform);
$response->noContent();
});
+198 -194
View File
@@ -4,25 +4,26 @@ require_once __DIR__ . '/../init.php';
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Key;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
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;
use Appwrite\Transformation\Adapter\Preview;
use Appwrite\Transformation\Transformation;
use Appwrite\Utopia\Database\Documents\User as DBUser;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Request\Filters\V16 as RequestV16;
use Appwrite\Utopia\Request\Filters\V17 as RequestV17;
use Appwrite\Utopia\Request\Filters\V18 as RequestV18;
use Appwrite\Utopia\Request\Filters\V19 as RequestV19;
use Appwrite\Utopia\Request\Filters\V20 as RequestV20;
use Appwrite\Utopia\Request\Filters\V21 as RequestV21;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
@@ -38,6 +39,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;
@@ -50,33 +52,36 @@ 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, ?Key $apiKey)
{
$host = $request->getHostname() ?? '';
if (!empty($previewHostname)) {
$host = $previewHostname;
}
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($host)));
} else {
$rule = Authorization::skip(
fn () => $dbForPlatform->find('rules', [
Query::equal('domain', [$host]),
Query::limit(1)
])
)[0] ?? new Document();
}
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$rule = Authorization::skip(function () use ($dbForPlatform, $host, $isMd5) {
if ($isMd5) {
return $dbForPlatform->getDocument('rules', md5($host));
}
return $dbForPlatform->findOne('rules', [
Query::equal('domain', [$host]),
]) ?? new Document();
});
$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', '');
@@ -97,10 +102,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
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);
}
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
@@ -142,7 +145,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);
}
@@ -222,7 +225,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
*/
$requirePreview = \is_null($apiKey) || !$apiKey->isPreviewAuthDisabled();
if ($isPreview && $requirePreview) {
$cookie = $request->getCookie(Auth::$cookieNamePreview, '');
$cookie = $request->getCookie(COOKIE_NAME_PREVIEW, '');
$authorized = false;
// Security checks to mark authorized true
@@ -267,7 +270,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')
@@ -455,14 +458,10 @@ 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";
// Appwrite vars
if ($type === 'function') {
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint,
'APPWRITE_FUNCTION_API_ENDPOINT' => $platform['endpoint'],
'APPWRITE_FUNCTION_ID' => $resource->getId(),
'APPWRITE_FUNCTION_NAME' => $resource->getAttribute('name'),
'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),
@@ -474,7 +473,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
]);
} elseif ($type === 'site') {
$vars = \array_merge($vars, [
'APPWRITE_SITE_API_ENDPOINT' => $endpoint,
'APPWRITE_SITE_API_ENDPOINT' => $platform['endpoint'],
'APPWRITE_SITE_ID' => $resource->getId(),
'APPWRITE_SITE_NAME' => $resource->getAttribute('name'),
'APPWRITE_SITE_DEPLOYMENT' => $deployment->getId(),
@@ -844,34 +843,31 @@ 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')
->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) {
/*
* 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, $apiKey)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -906,92 +902,9 @@ App::init()
$dbForProject = $getProjectDB($project);
$request->addFilter(new RequestV20($dbForProject, $route->getPathValues($request)));
}
}
$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
if (version_compare($requestFormat, '1.9.0', '<')) {
$request->addFilter(new RequestV21());
}
Config::setParam('domains', $domains);
}
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
@@ -999,7 +912,7 @@ App::init()
$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(
@@ -1028,8 +941,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', '<')) {
@@ -1049,14 +962,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.');
}
@@ -1064,49 +976,151 @@ 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')
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform) {
$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);
Console::warning($domain->get() . ' is not a publicly accessible domain. Skipping SSL certificate generation.');
return;
}
if (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) {
Console::warning('Skipping SSL certificates generation on ACME challenge.');
return;
}
// 3. Check if domain is a main domain
if (!in_array($domain->get(), $platformHostnames)) {
Console::warning($domain->get() . ' is not a main domain. Skipping SSL certificate generation.');
return;
}
// 4. Check/create rule (requires DB access)
Authorization::disable();
try {
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$document = $isMd5
? $dbForPlatform->getDocument('rules', md5($domain->get()))
: $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain->get()]),
]);
if (!$document->isEmpty()) {
return;
}
// 5. Create new rule
$owner = '';
$fallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', '');
$funcDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
$siteDomain = System::getEnv('_APP_DOMAIN_SITES', '');
if (!empty($fallback) && \str_ends_with($domain->get(), $fallback)) {
$funcDomain = $fallback;
}
if (
(!empty($funcDomain) && \str_ends_with($domain->get(), $funcDomain)) ||
(!empty($siteDomain) && \str_ends_with($domain->get(), $siteDomain))
) {
$owner = 'Appwrite';
}
$ruleId = $isMd5 ? md5($domain->get()) : ID::unique();
$document = new Document([
'$id' => $ruleId,
'domain' => $domain->get(),
'type' => 'api',
'status' => 'verifying',
'projectId' => $console->getId(),
'projectInternalId' => $console->getSequence(),
'search' => implode(' ', [$ruleId, $domain->get()]),
'owner' => $owner,
'region' => $console->getAttribute('region')
]);
$dbForPlatform->createDocument('rules', $document);
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
$queueForCertificates
->setDomain($document)
->setSkipRenewCheck(true)
->trigger();
} catch (Duplicate $e) {
Console::info('Certificate already exists');
} finally {
$cache[$domain->get()] = true;
Config::setParam('hostnames', $cache);
Authorization::reset();
}
});
App::options()
->inject('utopia')
->inject('swooleRequest')
@@ -1121,38 +1135,32 @@ 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')
->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) {
/*
* 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, $apiKey)) {
$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
*/
@@ -1256,7 +1264,7 @@ App::error()
* If not a publishable error, track usage stats. Publishable errors are >= 500 or those explicitly marked as publish=true in errors.php
*/
if (!$publish && $project->getId() !== 'console') {
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
if (!DBUser::isPrivileged(Authorization::getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
@@ -1439,18 +1447,16 @@ 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)) {
->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) {
$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, $apiKey)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1473,18 +1479,16 @@ 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)) {
->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) {
$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, $apiKey)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1613,7 +1617,7 @@ App::get('/_appwrite/authorize')
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$response
->addCookie(Auth::$cookieNamePreview, $jwt, (new \DateTime($expire))->getTimestamp(), '/', $host, ('https' === $protocol), true, null)
->addCookie(COOKIE_NAME_PREVIEW, $jwt, (new \DateTime($expire))->getTimestamp(), '/', $host, ('https' === $protocol), true, null)
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($protocol . '://' . $host . $path);
+2 -3
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')
+108 -28
View File
@@ -1,6 +1,5 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Auth\Key;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Event\Audit;
@@ -9,20 +8,22 @@ 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\Migration;
use Appwrite\Event\Realtime;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\SDK\Method;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Abuse\Abuse;
use Utopia\App;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@@ -34,7 +35,7 @@ use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Validator\WhiteList;
$parseLabel = function (string $label, array $responsePayload, array $requestParams, Document $user) {
$parseLabel = function (string $label, array $responsePayload, array $requestParams, User $user) {
preg_match_all('/{(.*?)}/', $label, $matches);
foreach ($matches[1] ?? [] as $pos => $match) {
$find = $matches[0][$pos];
@@ -232,42 +233,97 @@ App::init()
->inject('mode')
->inject('team')
->inject('apiKey')
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey) {
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey) {
$route = $utopia->getRoute();
/**
* Handle user authentication and session validation.
*
* This function follows a series of steps to determine the appropriate user session
* based on cookies, headers, and JWT tokens.
*
* Process:
*
* Project & Role Validation:
* 1. Check if the project is empty. If so, throw an exception.
* 2. Get the roles configuration.
* 3. Determine the role for the user based on the user document.
* 4. Get the scopes for the role.
*
* API Key Authentication:
* 5. If there is an API key:
* - Verify no user session exists simultaneously
* - Check if key is expired
* - Set role and scopes from API key
* - Handle special app role case
* - For standard keys, update last accessed time
*
* User Activity:
* 6. If the project is not the console and user is not admin:
* - Update user's last activity timestamp
*
* Access Control:
* 7. Get the method from the route
* 8. Validate namespace permissions
* 9. Validate scope permissions
* 10. Check if user is blocked
*
* Security Checks:
* 11. Verify password status (check if reset required)
* 12. Validate MFA requirements:
* - Check if MFA is enabled
* - Verify email status
* - Verify phone status
* - Verify authenticator status
* 13. Handle Multi-Factor Authentication:
* - Check remaining required factors
* - Validate factor completion
* - Throw exception if factors incomplete
*/
// Step 1: Check if project is empty
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
// Step 2: Get roles configuration
$roles = Config::getParam('roles', []);
// Step 3: Determine role for user
// TODO get scopes from the identity instead of the user roles config. The identity will containn the scopes the user authorized for the access token.
$role = $user->isEmpty()
? Role::guests()->toString()
: Role::users()->toString();
// Step 4: Get scopes for the role
$scopes = $roles[$role]['scopes'];
// API Key authentication
// Step 5: API Key Authentication
if (!empty($apiKey)) {
// Verify no user session exists simultaneously
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
}
// Check if key is expired
if ($apiKey->isExpired()) {
throw new Exception(Exception::PROJECT_KEY_EXPIRED);
}
// Set role and scopes from API key
$role = $apiKey->getRole();
$scopes = $apiKey->getScopes();
if ($apiKey->getRole() === Auth::USER_ROLE_APPS) {
// Handle special app role case
if ($apiKey->getRole() === User::ROLE_APPS) {
// Disable authorization checks for API keys
Authorization::setDefaultStatus(false);
$user = new Document([
$user = new User([
'$id' => '',
'status' => true,
'type' => Auth::ACTIVITY_TYPE_APP,
'type' => ACTIVITY_TYPE_APP,
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => $apiKey->getName(),
@@ -276,6 +332,7 @@ App::init()
$queueForAudits->setUser($user);
}
// For standard keys, update last accessed time
if ($apiKey->getType() === API_KEY_STANDARD) {
$dbKey = $project->find(
key: 'secret',
@@ -341,11 +398,11 @@ App::init()
$scopes = \array_unique($scopes);
Authorization::setRole($role);
foreach (Auth::getRoles($user) as $authRole) {
foreach ($user->getRoles() as $authRole) {
Authorization::setRole($authRole);
}
// Update project last activity
// Step 6: Update project and user last activity
if (!$project->isEmpty() && $project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
@@ -354,7 +411,6 @@ App::init()
}
}
// Update user last activity
if (!empty($user->getId())) {
$accessedAt = $user->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
@@ -368,6 +424,7 @@ App::init()
}
}
// Steps 7-9: Access Control - Method, Namespace and Scope Validation
/**
* @var ?Method $method
*/
@@ -385,27 +442,29 @@ App::init()
if (
array_key_exists($namespace, $project->getAttribute('services', []))
&& !$project->getAttribute('services', [])[$namespace]
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
) {
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
}
}
// Do now allow access if scope is not allowed
// Step 9: Validate scope permissions
$allowed = (array)$route->getLabel('scope', 'none');
if (empty(\array_intersect($allowed, $scopes))) {
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, $user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scopes (' . \json_encode($allowed) . ')');
}
// Do not allow access to blocked accounts
// Step 10: Check if user is blocked
if (false === $user->getAttribute('status')) { // Account is blocked
throw new Exception(Exception::USER_BLOCKED);
}
// Step 11: Verify password status
if ($user->getAttribute('reset')) {
throw new Exception(Exception::USER_PASSWORD_RESET_REQUIRED);
}
// Step 12: Validate MFA requirements
$mfaEnabled = $user->getAttribute('mfa', false);
$hasVerifiedEmail = $user->getAttribute('emailVerification', false);
$hasVerifiedPhone = $user->getAttribute('phoneVerification', false);
@@ -413,6 +472,7 @@ App::init()
$hasMoreFactors = $hasVerifiedEmail || $hasVerifiedPhone || $hasVerifiedAuthenticator;
$minimumFactors = ($mfaEnabled && $hasMoreFactors) ? 2 : 1;
// Step 13: Handle Multi-Factor Authentication
if (!in_array('mfa', $route->getGroups())) {
if ($session && \count($session->getAttribute('factors', [])) < $minimumFactors) {
throw new Exception(Exception::USER_MORE_FACTORS_REQUIRED);
@@ -437,6 +497,9 @@ App::init()
->inject('queueForDatabase')
->inject('queueForBuilds')
->inject('queueForStatsUsage')
->inject('queueForFunctions')
->inject('queueForMails')
->inject('queueForMigrations')
->inject('dbForProject')
->inject('timelimit')
->inject('resourceToken')
@@ -445,14 +508,15 @@ App::init()
->inject('plan')
->inject('devKey')
->inject('telemetry')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry) use ($usageDatabaseListener, $eventDatabaseListener) {
->inject('platform')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Mail $queueForMails, Migration $queueForMigrations, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform) use ($usageDatabaseListener, $eventDatabaseListener) {
$route = $utopia->getRoute();
if (
array_key_exists('rest', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['rest']
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
@@ -483,8 +547,8 @@ App::init()
$closestLimit = null;
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
$isPrivilegedUser = User::isPrivileged($roles);
$isAppUser = User::isApp($roles);
foreach ($timeLimitArray as $timeLimit) {
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
@@ -519,6 +583,10 @@ App::init()
}
}
/**
* TODO: (@loks0n)
* Avoid mutating the message across file boundaries - it's difficult to reason about at scale.
*/
/*
* Background Jobs
*/
@@ -539,7 +607,7 @@ App::init()
if (!$user->isEmpty()) {
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
$userClone->setAttribute('type', Auth::ACTIVITY_TYPE_USER);
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
$queueForAudits->setUser($userClone);
}
@@ -549,10 +617,18 @@ App::init()
}
}
/* Auto-set projects */
$queueForDeletes->setProject($project);
$queueForDatabase->setProject($project);
$queueForBuilds->setProject($project);
$queueForMessaging->setProject($project);
$queueForFunctions->setProject($project);
$queueForBuilds->setProject($project);
/* Auto-set platforms */
$queueForFunctions->setPlatform($platform);
$queueForBuilds->setPlatform($platform);
$queueForMails->setPlatform($platform);
$queueForMigrations->setPlatform($platform);
// Clone the queues, to prevent events triggered by the database listener
// from overwriting the events that are supposed to be triggered in the shutdown hook.
@@ -582,7 +658,7 @@ App::init()
if ($useCache) {
$route = $utopia->match($request);
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !Auth::isPrivilegedUser(Authorization::getRoles());
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !User::isPrivileged(Authorization::getRoles());
$key = $request->cacheIdentifier();
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
@@ -605,12 +681,16 @@ App::init()
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAppUser && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
if (!$bucket->getAttribute('transformations', true) && !$isAppUser && !$isPrivilegedUser) {
throw new Exception(Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED);
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_READ);
$valid = $validator->isValid($bucket->getRead());
@@ -635,7 +715,7 @@ App::init()
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
//Do not update transformedAt if it's a console user
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
if (!User::isPrivileged(Authorization::getRoles())) {
$transformedAt = $file->getAttribute('transformedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
$file->setAttribute('transformedAt', DateTime::now());
@@ -735,7 +815,7 @@ App::shutdown()
->inject('queueForWebhooks')
->inject('queueForRealtime')
->inject('dbForProject')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) use ($parseLabel) {
->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -783,7 +863,7 @@ App::shutdown()
if (!$user->isEmpty()) {
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
$userClone->setAttribute('type', Auth::ACTIVITY_TYPE_USER);
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
$queueForAudits->setUser($userClone);
} elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) {
/**
@@ -794,10 +874,10 @@ App::shutdown()
*
* Therefore, we consider this an anonymous request and create a relevant user.
*/
$user = new Document([
$user = new User([
'$id' => '',
'status' => true,
'type' => Auth::ACTIVITY_TYPE_GUEST,
'type' => ACTIVITY_TYPE_GUEST,
'email' => 'guest.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => 'Guest',
@@ -887,7 +967,7 @@ App::shutdown()
}
if ($project->getId() !== 'console') {
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
if (!User::isPrivileged(Authorization::getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
+4 -4
View File
@@ -1,7 +1,7 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use MaxMind\Db\Reader;
use Utopia\App;
@@ -20,7 +20,7 @@ App::init()
$lastUpdate = $session->getAttribute('mfaUpdatedAt');
if (!empty($lastUpdate)) {
$now = DateTime::now();
$maxAllowedDate = DateTime::addSeconds(new \DateTime($lastUpdate), Auth::MFA_RECENT_DURATION); // Maximum date until session is considered safe before asking for another challenge
$maxAllowedDate = DateTime::addSeconds(new \DateTime($lastUpdate), MFA_RECENT_DURATION); // Maximum date until session is considered safe before asking for another challenge
$isSessionFresh = DateTime::formatTz($maxAllowedDate) >= DateTime::formatTz($now);
}
@@ -49,8 +49,8 @@ App::init()
$route = $utopia->match($request);
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
$isAppUser = User::isApp(Authorization::getRoles());
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
return;
+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,
+40 -36
View File
@@ -1,42 +1,46 @@
<?php
use Utopia\Config\Adapters\PHP;
use Utopia\Config\Config;
require_once __DIR__ . '/../config/storage/resource_limits.php';
Config::load('template-runtimes', __DIR__ . '/../config/template-runtimes.php');
Config::load('events', __DIR__ . '/../config/events.php');
Config::load('auth', __DIR__ . '/../config/auth.php');
Config::load('apis', __DIR__ . '/../config/apis.php'); // List of APIs
Config::load('errors', __DIR__ . '/../config/errors.php');
Config::load('oAuthProviders', __DIR__ . '/../config/oAuthProviders.php');
Config::load('platforms', __DIR__ . '/../config/platforms.php');
Config::load('console', __DIR__ . '/../config/console.php');
Config::load('collections', __DIR__ . '/../config/collections.php');
Config::load('frameworks', __DIR__ . '/../config/frameworks.php');
Config::load('runtimes', __DIR__ . '/../config/runtimes.php');
Config::load('runtimes-v2', __DIR__ . '/../config/runtimes-v2.php');
Config::load('usage', __DIR__ . '/../config/usage.php');
Config::load('roles', __DIR__ . '/../config/roles.php'); // User roles and scopes
Config::load('scopes', __DIR__ . '/../config/scopes.php'); // User roles and scopes
Config::load('services', __DIR__ . '/../config/services.php'); // List of services
Config::load('variables', __DIR__ . '/../config/variables.php'); // List of env variables
Config::load('regions', __DIR__ . '/../config/regions.php'); // List of available regions
Config::load('avatar-browsers', __DIR__ . '/../config/avatars/browsers.php');
Config::load('avatar-credit-cards', __DIR__ . '/../config/avatars/credit-cards.php');
Config::load('avatar-flags', __DIR__ . '/../config/avatars/flags.php');
Config::load('locale-codes', __DIR__ . '/../config/locale/codes.php');
Config::load('locale-currencies', __DIR__ . '/../config/locale/currencies.php');
Config::load('locale-eu', __DIR__ . '/../config/locale/eu.php');
Config::load('locale-languages', __DIR__ . '/../config/locale/languages.php');
Config::load('locale-phones', __DIR__ . '/../config/locale/phones.php');
Config::load('locale-countries', __DIR__ . '/../config/locale/countries.php');
Config::load('locale-continents', __DIR__ . '/../config/locale/continents.php');
Config::load('locale-templates', __DIR__ . '/../config/locale/templates.php');
Config::load('storage-logos', __DIR__ . '/../config/storage/logos.php');
Config::load('storage-mimes', __DIR__ . '/../config/storage/mimes.php');
Config::load('storage-inputs', __DIR__ . '/../config/storage/inputs.php');
Config::load('storage-outputs', __DIR__ . '/../config/storage/outputs.php');
Config::load('specifications', __DIR__ . '/../config/specifications.php');
Config::load('templates-function', __DIR__ . '/../config/templates/function.php');
Config::load('templates-site', __DIR__ . '/../config/templates/site.php');
$configAdapter = new PHP();
Config::load('runtimes', __DIR__ . '/../config/runtimes.php', $configAdapter);
Config::load('runtimes-v2', __DIR__ . '/../config/runtimes-v2.php', $configAdapter);
Config::load('template-runtimes', __DIR__ . '/../config/template-runtimes.php', $configAdapter);
Config::load('events', __DIR__ . '/../config/events.php', $configAdapter);
Config::load('auth', __DIR__ . '/../config/auth.php', $configAdapter);
Config::load('apis', __DIR__ . '/../config/apis.php', $configAdapter); // List of APIs
Config::load('errors', __DIR__ . '/../config/errors.php', $configAdapter);
Config::load('oAuthProviders', __DIR__ . '/../config/oAuthProviders.php', $configAdapter);
Config::load('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('services', __DIR__ . '/../config/services.php', $configAdapter); // List of services
Config::load('variables', __DIR__ . '/../config/variables.php', $configAdapter); // List of env variables
Config::load('regions', __DIR__ . '/../config/regions.php', $configAdapter); // List of available regions
Config::load('avatar-browsers', __DIR__ . '/../config/avatars/browsers.php', $configAdapter);
Config::load('avatar-credit-cards', __DIR__ . '/../config/avatars/credit-cards.php', $configAdapter);
Config::load('avatar-flags', __DIR__ . '/../config/avatars/flags.php', $configAdapter);
Config::load('locale-codes', __DIR__ . '/../config/locale/codes.php', $configAdapter);
Config::load('locale-currencies', __DIR__ . '/../config/locale/currencies.php', $configAdapter);
Config::load('locale-eu', __DIR__ . '/../config/locale/eu.php', $configAdapter);
Config::load('locale-languages', __DIR__ . '/../config/locale/languages.php', $configAdapter);
Config::load('locale-phones', __DIR__ . '/../config/locale/phones.php', $configAdapter);
Config::load('locale-countries', __DIR__ . '/../config/locale/countries.php', $configAdapter);
Config::load('locale-continents', __DIR__ . '/../config/locale/continents.php', $configAdapter);
Config::load('locale-templates', __DIR__ . '/../config/locale/templates.php', $configAdapter);
Config::load('storage-logos', __DIR__ . '/../config/storage/logos.php', $configAdapter);
Config::load('storage-mimes', __DIR__ . '/../config/storage/mimes.php', $configAdapter);
Config::load('storage-inputs', __DIR__ . '/../config/storage/inputs.php', $configAdapter);
Config::load('storage-outputs', __DIR__ . '/../config/storage/outputs.php', $configAdapter);
Config::load('specifications', __DIR__ . '/../config/specifications.php', $configAdapter);
Config::load('templates-function', __DIR__ . '/../config/templates/function.php', $configAdapter);
Config::load('templates-site', __DIR__ . '/../config/templates/site.php', $configAdapter);
+74 -1
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';
@@ -81,7 +86,6 @@ 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;
@@ -93,6 +97,69 @@ const APP_VCS_GITHUB_EMAIL = 'team@appwrite.io';
const APP_VCS_GITHUB_URL = 'https://github.com/TeamAppwrite';
const APP_BRANDED_EMAIL_BASE_TEMPLATE = 'email-base-styled';
/**
* JWT for Resource Tokens.
*/
const RESOURCE_TOKEN_ALGORITHM = 'HS256';
const RESOURCE_TOKEN_MAX_AGE = 86400 * 365 * 10; /* 10 years */
const RESOURCE_TOKEN_LEEWAY = 10; // 10 seconds
/**
* Token Expiration times.
*/
const TOKEN_EXPIRATION_LOGIN_LONG = 31536000; /* 1 year */
const TOKEN_EXPIRATION_LOGIN_SHORT = 3600; /* 1 hour */
const TOKEN_EXPIRATION_RECOVERY = 3600; /* 1 hour */
const TOKEN_EXPIRATION_CONFIRM = 3600 * 1; /* 1 hour */
const TOKEN_EXPIRATION_OTP = 60 * 15; /* 15 minutes */
const TOKEN_EXPIRATION_GENERIC = 60 * 15; /* 15 minutes */
/**
* Token Lengths.
*/
const TOKEN_LENGTH_MAGIC_URL = 64;
const TOKEN_LENGTH_VERIFICATION = 256;
const TOKEN_LENGTH_RECOVERY = 256;
const TOKEN_LENGTH_OAUTH2 = 64;
const TOKEN_LENGTH_SESSION = 256;
/**
* Token Types.
*/
const TOKEN_TYPE_LOGIN = 1; // Deprecated
const TOKEN_TYPE_VERIFICATION = 2;
const TOKEN_TYPE_RECOVERY = 3;
const TOKEN_TYPE_INVITE = 4;
const TOKEN_TYPE_MAGIC_URL = 5;
const TOKEN_TYPE_PHONE = 6;
const TOKEN_TYPE_OAUTH2 = 7;
const TOKEN_TYPE_GENERIC = 8;
const TOKEN_TYPE_EMAIL = 9; // OTP
/**
* Session Providers.
*/
const SESSION_PROVIDER_EMAIL = 'email';
const SESSION_PROVIDER_ANONYMOUS = 'anonymous';
const SESSION_PROVIDER_MAGIC_URL = 'magic-url';
const SESSION_PROVIDER_PHONE = 'phone';
const SESSION_PROVIDER_OAUTH2 = 'oauth2';
const SESSION_PROVIDER_TOKEN = 'token';
const SESSION_PROVIDER_SERVER = 'server';
/**
* Activity associated with user or the app.
*/
const ACTIVITY_TYPE_APP = 'app';
const ACTIVITY_TYPE_USER = 'user';
const ACTIVITY_TYPE_GUEST = 'guest';
/**
* MFA
*/
const MFA_RECENT_DURATION = 1800; // 30 mins
// Database Reconnect
const DATABASE_RECONNECT_SLEEP = 2;
const DATABASE_RECONNECT_MAX_ATTEMPTS = 10;
@@ -138,6 +205,7 @@ const DELETE_TYPE_TOPIC = 'topic';
const DELETE_TYPE_TARGET = 'target';
const DELETE_TYPE_EXPIRED_TARGETS = 'invalid_targets';
const DELETE_TYPE_SESSION_TARGETS = 'session_targets';
const DELETE_TYPE_CSV_EXPORTS = 'csv_exports';
const DELETE_TYPE_MAINTENANCE = 'maintenance';
// Message types
@@ -271,6 +339,8 @@ const METRIC_SITES_ID_REQUESTS = 'sites.{siteInternalId}.requests';
const METRIC_SITES_ID_INBOUND = 'sites.{siteInternalId}.inbound';
const METRIC_SITES_ID_OUTBOUND = 'sites.{siteInternalId}.outbound';
const METRIC_AVATARS_SCREENSHOTS_GENERATED = 'avatars.screenshotsGenerated';
const METRIC_FUNCTIONS_RUNTIME = 'functions.runtimes.{runtime}';
const METRIC_SITES_FRAMEWORK = 'sites.frameworks.{framework}';
// Resource types
const RESOURCE_TYPE_PROJECTS = 'projects';
@@ -294,3 +364,6 @@ const TOKENS_RESOURCE_TYPE_DATABASES = 'databases';
const SCHEDULE_RESOURCE_TYPE_EXECUTION = 'execution';
const SCHEDULE_RESOURCE_TYPE_FUNCTION = 'function';
const SCHEDULE_RESOURCE_TYPE_MESSAGE = 'message';
/** Preview cookie */
const COOKIE_NAME_PREVIEW = 'a_jwt_console';
+47 -1
View File
@@ -39,6 +39,7 @@ if (!App::isProduction()) {
PublicDomain::allow(['request-catcher-sms']);
PublicDomain::allow(['request-catcher-webhook']);
}
$register->set('logger', function () {
// Register error logger
$providerName = System::getEnv('_APP_LOGGING_PROVIDER', '');
@@ -97,6 +98,51 @@ $register->set('logger', function () {
return new Logger($adapter);
});
$register->set('realtimeLogger', function () {
// Register error logger for realtime, falls back to default logging config
$providerConfig = System::getEnv('_APP_LOGGING_CONFIG_REALTIME', '')
?: System::getEnv('_APP_LOGGING_CONFIG', '');
if (empty($providerConfig)) {
return;
}
$loggingProvider = new DSN($providerConfig);
$providerName = $loggingProvider->getScheme();
$providerConfig = match ($providerName) {
'sentry' => ['key' => $loggingProvider->getPassword(), 'projectId' => $loggingProvider->getUser() ?? '', 'host' => 'https://' . $loggingProvider->getHost()],
'logowl' => ['ticket' => $loggingProvider->getUser() ?? '', 'host' => $loggingProvider->getHost()],
default => ['key' => $loggingProvider->getHost()],
};
if (empty($providerName) || empty($providerConfig)) {
return;
}
if (!Logger::hasProvider($providerName)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Logging provider not supported. Logging is disabled");
}
try {
$adapter = match ($providerName) {
'sentry' => new Sentry($providerConfig['projectId'], $providerConfig['key'], $providerConfig['host']),
'logowl' => new LogOwl($providerConfig['ticket'], $providerConfig['host']),
'raygun' => new Raygun($providerConfig['key']),
'appsignal' => new AppSignal($providerConfig['key']),
default => null
};
} catch (Throwable $th) {
$adapter = null;
}
if ($adapter === null) {
Console::error("Logging provider not supported. Logging is disabled");
return;
}
return new Logger($adapter);
});
$register->set('pools', function () {
$group = new Group();
@@ -326,7 +372,7 @@ $register->set('smtp', function () {
return $mail;
});
$register->set('geodb', function () {
return new Reader(__DIR__ . '/../assets/dbip/dbip-country-lite-2024-09.mmdb');
return new Reader(__DIR__ . '/../assets/dbip/dbip-country-lite-2025-12.mmdb');
});
$register->set('passwordsDictionary', function () {
$content = \file_get_contents(__DIR__ . '/../assets/security/10k-common-passwords');
+256 -161
View File
@@ -2,7 +2,6 @@
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Key;
use Appwrite\Databases\TransactionState;
use Appwrite\Event\Audit;
@@ -21,12 +20,22 @@ use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
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\Auth\Hashes\Argon2;
use Utopia\Auth\Hashes\Sha;
use Utopia\Auth\Proofs\Code;
use Utopia\Auth\Proofs\Password;
use Utopia\Auth\Proofs\Token;
use Utopia\Auth\Store;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
@@ -36,7 +45,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;
@@ -57,7 +65,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;
@@ -152,150 +160,256 @@ 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 (Request $request) {
$platform = Config::getParam('platform', []);
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$port = '';
if ($request->getPort() === '443' && $protocol !== 'https') {
$port = ':443';
}
if ($request->getPort() === '80' && $protocol !== 'http') {
$port = ':80';
}
$platform['endpoint'] = "$protocol://{$platform['apiHostname']}{$port}/v1";
// Add `exp` and `appwrite-callback-{projectId}` schemes
return $platform;
}, ['request']);
/**
* 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) {
$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']);
App::setResource('user', function ($mode, $project, $console, $request, $response, $dbForProject, $dbForPlatform) {
/** @var Appwrite\Utopia\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Database\Database $dbForPlatform */
/** @var string $mode */
/**
* 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',
// SDK generator
'X-SDK-Version',
'X-SDK-Name',
'X-SDK-Language',
'X-SDK-Platform',
'X-SDK-GraphQL',
// 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) {
/**
* Handles user authentication and session validation.
*
* This function follows a series of steps to determine the appropriate user session
* based on cookies, headers, and JWT tokens.
*
* Process:
* 1. Checks the cookie based on mode:
* - If in admin mode, uses console project id for key.
* - Otherwise, sets the key using the project ID
* 2. If no cookie is found, attempts to retrieve the fallback header `x-fallback-cookies`.
* - If this method is used, returns the header: `X-Debug-Fallback: true`.
* 3. Fetches the user document from the appropriate database based on the mode.
* 4. If the user document is empty or the session key cannot be verified, sets an empty user document.
* 5. Regardless of the results from steps 1-4, attempts to fetch the JWT token.
* 6. If the JWT user has a valid session ID, updates the user variable with the user from `projectDB`,
* overwriting the previous value.
*/
Authorization::setDefaultStatus(true);
Auth::setCookieName('a_session_' . $project->getId());
$store->setKey('a_session_' . $project->getId());
if (APP_MODE_ADMIN === $mode) {
Auth::setCookieName('a_session_' . $console->getId());
$store->setKey('a_session_' . $console->getId());
}
$session = Auth::decodeSession(
$store->decode(
$request->getCookie(
Auth::$cookieName, // Get sessions
$request->getCookie(Auth::$cookieName . '_legacy', '')
$store->getKey(), // Get sessions
$request->getCookie($store->getKey() . '_legacy', '')
)
);
// Get session from header for SSR clients
if (empty($session['id']) && empty($session['secret'])) {
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
$sessionHeader = $request->getHeader('x-appwrite-session', '');
if (!empty($sessionHeader)) {
$session = Auth::decodeSession($sessionHeader);
$store->decode($sessionHeader);
}
}
// Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies
if ($response) {
if ($response) { // if in http context - add debug header
$response->addHeader('X-Debug-Fallback', 'false');
}
if (empty($session['id']) && empty($session['secret'])) {
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
if ($response) {
$response->addHeader('X-Debug-Fallback', 'true');
}
$fallback = $request->getHeader('x-fallback-cookies', '');
$fallback = \json_decode($fallback, true);
$session = Auth::decodeSession(((isset($fallback[Auth::$cookieName])) ? $fallback[Auth::$cookieName] : ''));
$store->decode(((is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : ''));
}
Auth::$unique = $session['id'] ?? '';
Auth::$secret = $session['secret'] ?? '';
$user = new Document([]);
if (!empty(Auth::$unique)) {
if ($mode === APP_MODE_ADMIN) {
$user = $dbForPlatform->getDocument('users', Auth::$unique);
} elseif (!$project->isEmpty()) {
if ($project->getId() === 'console') {
$user = $dbForPlatform->getDocument('users', Auth::$unique);
} else {
$user = $dbForProject->getDocument('users', Auth::$unique);
$user = null;
if (APP_MODE_ADMIN === $mode) {
/** @var User $user */
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
} else {
if ($project->isEmpty()) {
$user = new User([]);
} else {
if (!empty($store->getProperty('id', ''))) {
if ($project->getId() === 'console') {
/** @var User $user */
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
} else {
/** @var User $user */
$user = $dbForProject->getDocument('users', $store->getProperty('id', ''));
}
}
}
}
if (
!$user ||
$user->isEmpty() // Check a document has been found in the DB
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret)
|| !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken)
) { // Validate user has valid login token
$user = new Document([]);
$user = new User([]);
}
// if (APP_MODE_ADMIN === $mode) {
// if ($user->find('teamInternalId', $project->getAttribute('teamInternalId'), 'memberships')) {
// Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users.
@@ -303,18 +417,14 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
// $user = new Document([]);
// }
// }
$authJWT = $request->getHeader('x-appwrite-jwt', '');
if (!empty($authJWT) && !$project->isEmpty()) { // JWT authentication
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
try {
$payload = $jwt->decode($authJWT);
} catch (JWTException $error) {
throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage());
}
$jwtUserId = $payload['userId'] ?? '';
if (!empty($jwtUserId)) {
if ($mode === APP_MODE_ADMIN) {
@@ -323,20 +433,18 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
$user = $dbForProject->getDocument('users', $jwtUserId);
}
}
$jwtSessionId = $payload['sessionId'] ?? '';
if (!empty($jwtSessionId)) {
if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token
$user = new Document([]);
$user = new User([]);
}
}
}
$dbForProject->setMetadata('user', $user->getId());
$dbForPlatform->setMetadata('user', $user->getId());
return $user;
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform']);
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken']);
App::setResource('project', function ($dbForPlatform, $request, $console) {
/** @var Appwrite\Utopia\Request $request */
@@ -354,31 +462,61 @@ App::setResource('project', function ($dbForPlatform, $request, $console) {
return $project;
}, ['dbForPlatform', 'request', 'console']);
App::setResource('session', function (Document $user) {
App::setResource('session', function (User $user, Store $store, Token $proofForToken) {
if ($user->isEmpty()) {
return;
}
$sessions = $user->getAttribute('sessions', []);
$sessionId = Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret);
$sessionId = $user->sessionVerify($store->getProperty('secret', ''), $proofForToken);
if (!$sessionId) {
return;
}
foreach ($sessions as $session) {/** @var Document $session */
foreach ($sessions as $session) {
/** @var Document $session */
if ($sessionId === $session->getId()) {
return $session;
}
}
return;
}, ['user']);
}, ['user', 'store', 'proofForToken']);
App::setResource('console', function () {
return new Document(Config::getParam('console'));
}, []);
App::setResource('store', function (): Store {
return new Store();
});
App::setResource('proofForPassword', function (): Password {
$hash = new Argon2();
$hash
->setMemoryCost(7168)
->setTimeCost(5)
->setThreads(1);
$password = new Password();
$password
->setHash($hash);
return $password;
});
App::setResource('proofForToken', function (): Token {
$token = new Token();
$token->setHash(new Sha());
return $token;
});
App::setResource('proofForCode', function (): Code {
$code = new Code();
$code->setHash(new Sha());
return $code;
});
App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
@@ -399,6 +537,7 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform
->setMetadata('project', $project->getId())
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
$database->setDocumentType('users', User::class);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
@@ -428,6 +567,8 @@ App::setResource('dbForPlatform', function (Group $pools, Cache $cache) {
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
$database->setDocumentType('users', User::class);
return $database;
}, ['pools', 'cache']);
@@ -452,6 +593,7 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
->setMetadata('project', $project->getId())
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
$database->setDocumentType('users', User::class);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
@@ -688,7 +830,7 @@ App::setResource('passwordsDictionary', function ($register) {
App::setResource('servers', function () {
$platforms = Config::getParam('platforms');
$platforms = Config::getParam('sdks');
$server = $platforms[APP_PLATFORM_SERVER];
$languages = array_map(function ($language) {
@@ -789,24 +931,6 @@ App::setResource('schema', function ($utopia, $dbForProject) {
);
}, ['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;
});
App::setResource('gitHub', function (Cache $cache) {
return new VcsGitHub($cache);
}, ['cache']);
@@ -873,6 +997,7 @@ App::setResource('devKey', function (Request $request, Document $project, array
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
}
return $key;
}, ['request', 'project', 'servers', 'dbForPlatform']);
@@ -953,7 +1078,8 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
$tokenJWT = $request->getParam('token');
if (!empty($tokenJWT) && !$project->isEmpty()) { // JWT authentication
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway.
// Use a large but reasonable maxAge to avoid auto-exp when token has no expiry
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), RESOURCE_TOKEN_ALGORITHM, RESOURCE_TOKEN_MAX_AGE, RESOURCE_TOKEN_LEEWAY); // Instantiate with key, algo, maxAge and leeway.
try {
$payload = $jwt->decode($tokenJWT);
@@ -1012,37 +1138,6 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
return new Document([]);
}, ['project', 'dbForProject', 'request']);
App::setResource('httpReferrer', function (Request $request): string {
$referrer = $request->getReferer();
return $referrer;
}, ['request']);
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;
}
// 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']);
+70 -50
View File
@@ -1,13 +1,14 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Network\Validator\Origin;
use Appwrite\PubSub\Adapter\Pool as PubSubPool;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Swoole\Coroutine;
use Swoole\Http\Request as SwooleRequest;
use Swoole\Http\Response as SwooleResponse;
use Swoole\Runtime;
@@ -16,6 +17,9 @@ use Swoole\Timer;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\App;
use Utopia\Auth\Hashes\Sha;
use Utopia\Auth\Proofs\Token;
use Utopia\Auth\Store;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
@@ -49,14 +53,14 @@ Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
if (!function_exists('getConsoleDB')) {
function getConsoleDB(): Database
{
global $register;
$ctx = Coroutine::getContext();
static $database = null;
if ($database !== null) {
return $database;
if (isset($ctx['dbForPlatform'])) {
return $ctx['dbForPlatform'];
}
global $register;
/** @var Group $pools */
$pools = $register->get('pools');
@@ -66,8 +70,8 @@ if (!function_exists('getConsoleDB')) {
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', '_console');
return $database;
$database->setDocumentType('users', User::class);
return $ctx['dbForPlatform'] = $database;
}
}
@@ -75,14 +79,18 @@ if (!function_exists('getConsoleDB')) {
if (!function_exists('getProjectDB')) {
function getProjectDB(Document $project): Database
{
global $register;
$ctx = Coroutine::getContext();
static $databases = [];
if (isset($databases[$project->getSequence()])) {
return $databases[$project->getSequence()];
if (!isset($ctx['dbForProject'])) {
$ctx['dbForProject'] = [];
}
if (isset($ctx['dbForProject'][$project->getSequence()])) {
return $ctx['dbForProject'][$project->getSequence()];
}
global $register;
/** @var Group $pools */
$pools = $register->get('pools');
@@ -118,7 +126,9 @@ if (!function_exists('getProjectDB')) {
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId());
return $databases[$project->getSequence()] = $database;
$database->setDocumentType('users', User::class);
return $ctx['dbForProject'][$project->getSequence()] = $database;
}
}
@@ -126,14 +136,14 @@ if (!function_exists('getProjectDB')) {
if (!function_exists('getCache')) {
function getCache(): Cache
{
global $register;
$ctx = Coroutine::getContext();
static $cache = null;
if ($cache !== null) {
return $cache;
if (isset($ctx['cache'])) {
return $ctx['cache'];
}
global $register;
$pools = $register->get('pools'); /** @var Group $pools */
$list = Config::getParam('pools-cache', []);
@@ -143,7 +153,7 @@ if (!function_exists('getCache')) {
$adapters[] = new CachePool($pools->get($value));
}
return $cache = new Cache(new Sharding($adapters));
return $ctx['cache'] = new Cache(new Sharding($adapters));
}
}
@@ -151,10 +161,10 @@ if (!function_exists('getCache')) {
if (!function_exists('getRedis')) {
function getRedis(): \Redis
{
static $redis = null;
$ctx = Coroutine::getContext();
if ($redis !== null) {
return $redis;
if (isset($ctx['redis'])) {
return $ctx['redis'];
}
$host = System::getEnv('_APP_REDIS_HOST', 'localhost');
@@ -168,46 +178,46 @@ if (!function_exists('getRedis')) {
}
$redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
return $redis;
return $ctx['redis'] = $redis;
}
}
if (!function_exists('getTimelimit')) {
function getTimelimit(): TimeLimitRedis
function getTimelimit(string $key = "", int $limit = 0, int $seconds = 1): TimeLimitRedis
{
static $timelimit = null;
$ctx = Coroutine::getContext();
if ($timelimit !== null) {
return $timelimit;
if (isset($ctx['timelimit'])) {
return $ctx['timelimit'];
}
return $timelimit = new TimeLimitRedis("", 0, 1, getRedis());
return $ctx['timelimit'] = new TimeLimitRedis($key, $limit, $seconds, getRedis());
}
}
if (!function_exists('getRealtime')) {
function getRealtime(): Realtime
{
static $realtime = null;
$ctx = Coroutine::getContext();
if ($realtime !== null) {
return $realtime;
if (isset($ctx['realtime'])) {
return $ctx['realtime'];
}
return $realtime = new Realtime();
return $ctx['realtime'] = new Realtime();
}
}
if (!function_exists('getTelemetry')) {
function getTelemetry(int $workerId): Utopia\Telemetry\Adapter
{
static $telemetry = null;
$ctx = Coroutine::getContext();
if ($telemetry !== null) {
return $telemetry;
if (isset($ctx['telemetry'])) {
return $ctx['telemetry'];
}
return $telemetry = new NoTelemetry();
return $ctx['telemetry'] = new NoTelemetry();
}
}
@@ -236,7 +246,7 @@ $adapter
$server = new Server($adapter);
$logError = function (Throwable $error, string $action) use ($register) {
$logger = $register->get('logger');
$logger = $register->get('realtimeLogger');
if ($logger && !$error instanceof Exception) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
@@ -457,9 +467,10 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$project = Authorization::skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
$database = getProjectDB($project);
/** @var Appwrite\Utopia\Database\Documents\User $user */
$user = $database->getDocument('users', $userId);
$roles = Auth::getRoles($user);
$roles = $user->getRoles();
$channels = $realtime->connections[$connection]['channels'];
$realtime->unsubscribe($connection);
@@ -526,14 +537,13 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
if (
array_key_exists('realtime', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['realtime']
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
$timelimit = $app->getResource('timelimit');
$platforms = $app->getResource('platforms');
$user = $app->getResource('user'); /** @var Document $user */
$user = $app->getResource('user'); /** @var User $user */
/*
* Abuse Check
@@ -557,13 +567,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 = Auth::getRoles($user);
$roles = $user->getRoles();
$channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId());
@@ -678,21 +688,31 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
}
$session = Auth::decodeSession($message['data']['session']);
Auth::$unique = $session['id'] ?? '';
Auth::$secret = $session['secret'] ?? '';
$store = new Store();
$user = $database->getDocument('users', Auth::$unique);
$store->decode($message['data']['session']);
/** @var User $user */
$user = $database->getDocument('users', $store->getProperty('id', ''));
/**
* TODO:
* Moving forward, we should try to use our dependency injection container
* to inject the proof for token.
* This way we will have one source of truth for the proof for token.
*/
$proofForToken = new Token();
$proofForToken->setHash(new Sha());
if (
empty($user->getId()) // Check a document has been found in the DB
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret) // Validate user has valid login token
|| !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken) // Validate user has valid login token
) {
// cookie not valid
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Session is not valid.');
}
$roles = Auth::getRoles($user);
$roles = $user->getRoles();
$channels = Realtime::convertChannels(array_flip($realtime->connections[$connection]['channels']), $user->getId());
$realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels);
+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>
+3 -3
View File
@@ -849,7 +849,7 @@ $image = $this->getParam('image', '');
- _APP_DB_PASS
appwrite-assistant:
image: appwrite/assistant:0.8.3
image: appwrite/assistant:0.8.4
container_name: appwrite-assistant
<<: *x-logging
restart: unless-stopped
@@ -857,9 +857,9 @@ $image = $this->getParam('image', '');
- appwrite
environment:
- _APP_ASSISTANT_OPENAI_API_KEY
appwrite-browser:
image: appwrite/browser:0.3.1
image: appwrite/browser:0.3.2
container_name: appwrite-browser
<<: *x-logging
restart: unless-stopped
+3 -1
View File
@@ -17,6 +17,7 @@ use Appwrite\Event\Realtime;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Platform\Appwrite;
use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Swoole\Runtime;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
@@ -55,7 +56,7 @@ Server::setResource('dbForPlatform', function (Cache $cache, Registry $register)
$adapter = new DatabasePool($pools->get('console'));
$dbForPlatform = new Database($adapter, $cache);
$dbForPlatform->setNamespace('_console');
$dbForPlatform->setDocumentType('users', User::class);
return $dbForPlatform;
}, ['cache', 'register']);
@@ -86,6 +87,7 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$database->setDocumentType('users', User::class);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
+5 -4
View File
@@ -48,14 +48,15 @@
"utopia-php/abuse": "1.*",
"utopia-php/analytics": "0.10.*",
"utopia-php/audit": "1.*",
"utopia-php/auth": "0.5.*",
"utopia-php/cache": "0.13.*",
"utopia-php/cli": "0.15.*",
"utopia-php/config": "0.2.*",
"utopia-php/config": "1.*.*",
"utopia-php/database": "3.*",
"utopia-php/detector": "0.2.*",
"utopia-php/domains": "0.9.*",
"utopia-php/emails": "0.6.*",
"utopia-php/dns": "1.1.*",
"utopia-php/dns": "1.4.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/framework": "0.33.*",
"utopia-php/fetch": "0.4.*",
@@ -63,7 +64,7 @@
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.20.*",
"utopia-php/migration": "1.*",
"utopia-php/migration": "1.3.*",
"utopia-php/orchestration": "0.9.*",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "0.8.*",
@@ -74,7 +75,7 @@
"utopia-php/swoole": "0.8.*",
"utopia-php/system": "0.9.*",
"utopia-php/telemetry": "0.1.*",
"utopia-php/vcs": "0.12.*",
"utopia-php/vcs": "0.13.*",
"utopia-php/websocket": "0.3.*",
"matomo/device-detector": "6.4.*",
"dragonmantank/cron-expression": "3.4.*",
Generated
+353 -301
View File
File diff suppressed because it is too large Load Diff
+123 -7
View File
@@ -38,9 +38,15 @@ services:
depends_on:
- appwrite
networks:
- gateway
- appwrite
- runtimes
appwrite:
aliases:
- appwrite.test
gateway:
aliases:
- appwrite.test
runtimes:
aliases:
- appwrite.test
appwrite:
container_name: appwrite
@@ -48,6 +54,7 @@ services:
image: appwrite-dev
build:
context: .
target: development
args:
DEBUG: false
TESTING: true
@@ -285,6 +292,7 @@ services:
- _APP_DB_PASS
- _APP_USAGE_STATS
- _APP_LOGGING_CONFIG
- _APP_LOGGING_CONFIG_REALTIME
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-audits:
@@ -950,7 +958,7 @@ services:
appwrite-assistant:
container_name: appwrite-assistant
image: appwrite/assistant:0.8.3
image: appwrite/assistant:0.8.4
networks:
- appwrite
environment:
@@ -958,7 +966,7 @@ services:
appwrite-browser:
container_name: appwrite-browser
image: appwrite/browser:0.3.1
image: appwrite/browser:0.3.2
networks:
- appwrite
@@ -1083,13 +1091,26 @@ services:
# GraphQl Explorer - A nice UI for exploring GraphQL API
maildev: # used mainly for dev tests
image: appwrite/mailcatcher:1.0.0
image: appwrite/mailcatcher:1.1.1
container_name: appwrite-mailcatcher
<<: *x-logging
ports:
- "9503:1080"
networks:
- appwrite
- gateway
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_maildev.loadbalancer.server.port=1080"
- "traefik.http.routers.appwrite_maildev_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_maildev_http.rule=Host(`mail.localhost`)"
- "traefik.http.routers.appwrite_maildev_http.service=appwrite_maildev"
- "traefik.http.routers.appwrite_maildev_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_maildev_https.rule=Host(`mail.localhost`)"
- "traefik.http.routers.appwrite_maildev_https.service=appwrite_maildev"
- "traefik.http.routers.appwrite_maildev_https.tls=true"
request-catcher-webhook: # used mainly for dev tests (mock HTTP webhook)
image: appwrite/requestcatcher:1.0.0
@@ -1118,14 +1139,54 @@ services:
- 9506:8080
networks:
- appwrite
- gateway
environment:
- ADMINER_DESIGN=pepa-linha
- ADMINER_DEFAULT_SERVER=mariadb
- ADMINER_DEFAULT_USERNAME=root
- ADMINER_DEFAULT_PASSWORD=rootsecretpassword
- ADMINER_DEFAULT_DB=appwrite
configs:
- source: adminer-index.php
target: /var/www/html/index.php
mode: 0755
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_adminer.loadbalancer.server.port=8080"
- "traefik.http.routers.appwrite_adminer_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_adminer_http.rule=Host(`mysql.localhost`)"
- "traefik.http.routers.appwrite_adminer_http.service=appwrite_adminer"
- "traefik.http.routers.appwrite_adminer_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_adminer_https.rule=Host(`mysql.localhost`)"
- "traefik.http.routers.appwrite_adminer_https.service=appwrite_adminer"
- "traefik.http.routers.appwrite_adminer_https.tls=true"
redis-insight:
image: redis/redisinsight:latest
restart: unless-stopped
networks:
- appwrite
- gateway
environment:
- REDIS_HOSTS=redis
- RI_PRE_SETUP_DATABASES_PATH=/mnt/connections.json
configs:
- source: redisinsight-connections.json
target: /mnt/connections.json
mode: 0755
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_redisinsight.loadbalancer.server.port=5540"
- "traefik.http.routers.appwrite_redisinsight_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_redisinsight_http.rule=Host(`redis.localhost`)"
- "traefik.http.routers.appwrite_redisinsight_http.service=appwrite_redisinsight"
- "traefik.http.routers.appwrite_redisinsight_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_redisinsight_https.rule=Host(`redis.localhost`)"
- "traefik.http.routers.appwrite_redisinsight_https.service=appwrite_redisinsight"
- "traefik.http.routers.appwrite_redisinsight_https.tls=true"
ports:
- "8081:5540"
@@ -1150,6 +1211,61 @@ networks:
runtimes:
name: runtimes
configs:
redisinsight-connections.json:
content: |
[
{
"compressor": "NONE",
"id": "104dc90a-21ef-4d5e-8912-b30baabb152f",
"host": "redis",
"port": 6379,
"name": "redis:6379",
"db": 0,
"username": "default",
"password": null,
"connectionType": "STANDALONE",
"nameFromProvider": null,
"provider": "REDIS",
"lastConnection": "2025-10-16T09:22:02.591Z",
"modules": [
{
"name": "ReJSON",
"version": 20808,
"semanticVersion": "2.8.8"
},
{
"name": "search",
"version": 21015,
"semanticVersion": "2.10.15"
}
],
"tls": false,
"tlsServername": null,
"verifyServerCert": null,
"caCert": null,
"clientCert": null,
"ssh": false,
"sshOptions": null,
"forceStandalone": false,
"tags": []
}
]
adminer-index.php:
content: |
<?php
if(!count($$_GET)) {
$$_POST['auth'] = [
'server' => $$_ENV['ADMINER_DEFAULT_SERVER'],
'driver' => 'server', /* seems to autodetect the driver from server settings */
'username' => $$_ENV['ADMINER_DEFAULT_USERNAME'],
'password' => $$_ENV['ADMINER_DEFAULT_PASSWORD'],
'db' => $$_ENV['ADMINER_DEFAULT_DB'],
];
}
include './adminer.php';
volumes:
appwrite-mariadb:
appwrite-redis:
@@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
@@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: &lt;REGION&gt;.cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0
@@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
@@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
@@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: &lt;REGION&gt;.cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0
@@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
@@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: &lt;REGION&gt;.cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0
@@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
@@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
@@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: &lt;REGION&gt;.cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0
@@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0
@@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0
@@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0
@@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.6.0
@@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.7.0
@@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.7.0
@@ -1,4 +1,4 @@
POST /v1/account/mfa/challenge HTTP/1.1
POST /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.7.0
@@ -1,4 +1,4 @@
PUT /v1/account/mfa/challenge HTTP/1.1
PUT /v1/account/mfa/challenges HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.7.0
@@ -13,7 +13,7 @@ account.createOAuth2Session(
OAuthProvider.AMAZON, // provider
"https://example.com", // success (optional)
"https://example.com", // failure (optional)
listOf(), // scopes (optional)
List.of(), // scopes (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -13,7 +13,7 @@ account.createOAuth2Token(
OAuthProvider.AMAZON, // provider
"https://example.com", // success (optional)
"https://example.com", // failure (optional)
listOf(), // scopes (optional)
List.of(), // scopes (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -9,7 +9,7 @@ Client client = new Client(context)
Account account = new Account(client);
account.listIdentities(
listOf(), // queries (optional)
List.of(), // queries (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -9,7 +9,7 @@ Client client = new Client(context)
Account account = new Account(client);
account.listLogs(
listOf(), // queries (optional)
List.of(), // queries (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -9,10 +9,10 @@ Client client = new Client(context)
Account account = new Account(client);
account.updatePrefs(
mapOf(
"language" to "en",
"timezone" to "UTC",
"darkTheme" to true
Map.of(
"language", "en",
"timezone", "UTC",
"darkTheme", true
), // prefs
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -0,0 +1,47 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Avatars;
import io.appwrite.enums.Theme;
import io.appwrite.enums.Timezone;
import io.appwrite.enums.Output;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
.setProject("<YOUR_PROJECT_ID>"); // Your project ID
Avatars avatars = new Avatars(client);
avatars.getScreenshot(
"https://example.com", // url
Map.of(
"Authorization", "Bearer token123",
"X-Custom-Header", "value"
), // headers (optional)
1920, // viewportWidth (optional)
1080, // viewportHeight (optional)
2, // scale (optional)
Theme.LIGHT, // theme (optional)
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15", // userAgent (optional)
true, // fullpage (optional)
"en-US", // locale (optional)
Timezone.AFRICA_ABIDJAN, // timezone (optional)
37.7749, // latitude (optional)
-122.4194, // longitude (optional)
100, // accuracy (optional)
true, // touch (optional)
List.of("geolocation", "notifications"), // permissions (optional)
3, // sleep (optional)
800, // width (optional)
600, // height (optional)
85, // quality (optional)
Output.JPG, // output (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
return;
}
Log.d("Appwrite", result.toString());
})
);
@@ -1,8 +1,8 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Databases;
import io.appwrite.Permission;
import io.appwrite.Role;
import io.appwrite.services.Databases;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -14,14 +14,14 @@ databases.createDocument(
"<DATABASE_ID>", // databaseId
"<COLLECTION_ID>", // collectionId
"<DOCUMENT_ID>", // documentId
mapOf(
"username" to "walter.obrien",
"email" to "walter.obrien@example.com",
"fullName" to "Walter O'Brien",
"age" to 30,
"isAdmin" to false
Map.of(
"username", "walter.obrien",
"email", "walter.obrien@example.com",
"fullName", "Walter O'Brien",
"age", 30,
"isAdmin", false
), // data
listOf(Permission.read(Role.any())), // permissions (optional)
List.of(Permission.read(Role.any())), // permissions (optional)
"<TRANSACTION_ID>", // transactionId (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -10,17 +10,15 @@ Databases databases = new Databases(client);
databases.createOperations(
"<TRANSACTION_ID>", // transactionId
listOf(
{
"action": "create",
"databaseId": "<DATABASE_ID>",
"collectionId": "<COLLECTION_ID>",
"documentId": "<DOCUMENT_ID>",
"data": {
"name": "Walter O'Brien"
}
}
), // operations (optional)
List.of(Map.of(
"action", "create",
"databaseId", "<DATABASE_ID>",
"collectionId", "<COLLECTION_ID>",
"documentId", "<DOCUMENT_ID>",
"data", Map.of(
"name", "Walter O'Brien"
)
)), // operations (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -12,7 +12,7 @@ databases.getDocument(
"<DATABASE_ID>", // databaseId
"<COLLECTION_ID>", // collectionId
"<DOCUMENT_ID>", // documentId
listOf(), // queries (optional)
List.of(), // queries (optional)
"<TRANSACTION_ID>", // transactionId (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -11,7 +11,7 @@ Databases databases = new Databases(client);
databases.listDocuments(
"<DATABASE_ID>", // databaseId
"<COLLECTION_ID>", // collectionId
listOf(), // queries (optional)
List.of(), // queries (optional)
"<TRANSACTION_ID>", // transactionId (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
@@ -9,7 +9,7 @@ Client client = new Client(context)
Databases databases = new Databases(client);
databases.listTransactions(
listOf(), // queries (optional)
List.of(), // queries (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -1,8 +1,8 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Databases;
import io.appwrite.Permission;
import io.appwrite.Role;
import io.appwrite.services.Databases;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -14,8 +14,8 @@ databases.updateDocument(
"<DATABASE_ID>", // databaseId
"<COLLECTION_ID>", // collectionId
"<DOCUMENT_ID>", // documentId
mapOf( "a" to "b" ), // data (optional)
listOf(Permission.read(Role.any())), // permissions (optional)
Map.of("a", "b"), // data (optional)
List.of(Permission.read(Role.any())), // permissions (optional)
"<TRANSACTION_ID>", // transactionId (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -1,8 +1,8 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Databases;
import io.appwrite.Permission;
import io.appwrite.Role;
import io.appwrite.services.Databases;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -14,8 +14,8 @@ databases.upsertDocument(
"<DATABASE_ID>", // databaseId
"<COLLECTION_ID>", // collectionId
"<DOCUMENT_ID>", // documentId
mapOf( "a" to "b" ), // data
listOf(Permission.read(Role.any())), // permissions (optional)
Map.of("a", "b"), // data
List.of(Permission.read(Role.any())), // permissions (optional)
"<TRANSACTION_ID>", // transactionId (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -1,6 +1,7 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Functions;
import io.appwrite.enums.ExecutionMethod;
Client client = new Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
@@ -14,7 +15,7 @@ functions.createExecution(
false, // async (optional)
"<PATH>", // path (optional)
ExecutionMethod.GET, // method (optional)
mapOf( "a" to "b" ), // headers (optional)
Map.of("a", "b"), // headers (optional)
"<SCHEDULED_AT>", // scheduledAt (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -10,7 +10,7 @@ Functions functions = new Functions(client);
functions.listExecutions(
"<FUNCTION_ID>", // functionId
listOf(), // queries (optional)
List.of(), // queries (optional)
false, // total (optional)
new CoroutineCallback<>((result, error) -> {
if (error != null) {
@@ -9,7 +9,7 @@ Client client = new Client(context)
Graphql graphql = new Graphql(client);
graphql.mutation(
mapOf( "a" to "b" ), // query
Map.of("a", "b"), // query
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
@@ -9,7 +9,7 @@ Client client = new Client(context)
Graphql graphql = new Graphql(client);
graphql.query(
mapOf( "a" to "b" ), // query
Map.of("a", "b"), // query
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();

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