diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe2f61bfcf..3a6ae039f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,6 @@ concurrency: env: COMPOSE_FILE: docker-compose.yml IMAGE: appwrite-dev - CACHE_KEY: appwrite-dev-${{ github.event.pull_request.head.sha }} on: pull_request: @@ -248,11 +247,12 @@ jobs: TESTING=true VERSION=dev - - name: Cache Docker Image - uses: actions/cache@v5 + - name: Upload Docker Image + uses: actions/upload-artifact@v7 with: - key: ${{ env.CACHE_KEY }} + name: ${{ env.IMAGE }} path: /tmp/${{ env.IMAGE }}.tar + retention-days: 1 unit: name: Tests / Unit @@ -265,12 +265,11 @@ jobs: - name: checkout uses: actions/checkout@v6 - - name: Load Cache - uses: actions/cache@v5 + - name: Download Docker Image + uses: actions/download-artifact@v7 with: - key: ${{ env.CACHE_KEY }} - path: /tmp/${{ env.IMAGE }}.tar - fail-on-cache-miss: true + name: ${{ env.IMAGE }} + path: /tmp - name: Login to Docker Hub uses: docker/login-action@v4 @@ -313,12 +312,11 @@ jobs: - name: checkout uses: actions/checkout@v6 - - name: Load Cache - uses: actions/cache@v5 + - name: Download Docker Image + uses: actions/download-artifact@v7 with: - key: ${{ env.CACHE_KEY }} - path: /tmp/${{ env.IMAGE }}.tar - fail-on-cache-miss: true + name: ${{ env.IMAGE }} + path: /tmp - name: Login to Docker Hub uses: docker/login-action@v4 @@ -363,7 +361,7 @@ jobs: e2e_service: name: Tests / E2E / ${{ matrix.database }} (${{ matrix.mode }}) / ${{ matrix.service }} - runs-on: ubuntu-latest + runs-on: ${{ matrix.runner || 'ubuntu-latest' }} needs: [build, matrix] permissions: contents: read @@ -398,16 +396,28 @@ jobs: Messaging, Migrations ] + include: + - service: Databases + runner: blacksmith-4vcpu-ubuntu-2404 + - service: Sites + runner: blacksmith-4vcpu-ubuntu-2404 + - service: Functions + runner: blacksmith-4vcpu-ubuntu-2404 + - service: Avatars + runner: blacksmith-4vcpu-ubuntu-2404 + - service: Realtime + runner: blacksmith-4vcpu-ubuntu-2404 + - service: TablesDB + runner: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository uses: actions/checkout@v6 - - name: Load Cache - uses: actions/cache@v5 + - name: Download Docker Image + uses: actions/download-artifact@v7 with: - key: ${{ env.CACHE_KEY }} - path: /tmp/${{ env.IMAGE }}.tar - fail-on-cache-miss: true + name: ${{ env.IMAGE }} + path: /tmp - name: Set database environment run: | @@ -496,12 +506,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 - - name: Load Cache - uses: actions/cache@v5 + - name: Download Docker Image + uses: actions/download-artifact@v7 with: - key: ${{ env.CACHE_KEY }} - path: /tmp/${{ env.IMAGE }}.tar - fail-on-cache-miss: true + name: ${{ env.IMAGE }} + path: /tmp - name: Login to Docker Hub uses: docker/login-action@v4 @@ -555,12 +564,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 - - name: Load Cache - uses: actions/cache@v5 + - name: Download Docker Image + uses: actions/download-artifact@v7 with: - key: ${{ env.CACHE_KEY }} - path: /tmp/${{ env.IMAGE }}.tar - fail-on-cache-miss: true + name: ${{ env.IMAGE }} + path: /tmp - name: Login to Docker Hub uses: docker/login-action@v4 @@ -616,12 +624,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 - - name: Load Cache - uses: actions/cache@v5 + - name: Download Docker Image + uses: actions/download-artifact@v7 with: - key: ${{ env.CACHE_KEY }} - path: /tmp/${{ env.IMAGE }}.tar - fail-on-cache-miss: true + name: ${{ env.IMAGE }} + path: /tmp - name: Login to Docker Hub uses: docker/login-action@v4 diff --git a/CHANGES.md b/CHANGES.md index e6dd04b556..548c0d72b0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,100 +1,133 @@ -# Version 1.8.1 +# Version 1.9.0 ## What's Changed ### Notable changes -* Add branch deployments support in [#10486](https://github.com/appwrite/appwrite/pull/10486) -* Add TanStack Start sites support in [#10681](https://github.com/appwrite/appwrite/pull/10681) -* Add Next.js standalone support in [#10747](https://github.com/appwrite/appwrite/pull/10747) -* Add Resend integration in [#10690](https://github.com/appwrite/appwrite/pull/10690) -* Add option to enable/disable image transformations per-bucket in [#10722](https://github.com/appwrite/appwrite/pull/10722) -* Add operators support in [#10735](https://github.com/appwrite/appwrite/pull/10735) and [#10800](https://github.com/appwrite/appwrite/pull/10800) -* Add function and sites stats in [#10786](https://github.com/appwrite/appwrite/pull/10786) -* Add disable count feature in [#10668](https://github.com/appwrite/appwrite/pull/10668) -* Add ElevenLabs site template in [#10782](https://github.com/appwrite/appwrite/pull/10782) -* Add suggested environment variables in [#10795](https://github.com/appwrite/appwrite/pull/10795) -* Update GeoDB database in [#10890](https://github.com/appwrite/appwrite/pull/10890) -* Update Flutter default build runtime in [#10807](https://github.com/appwrite/appwrite/pull/10807) -* Upgrade runtimes in [#10804](https://github.com/appwrite/appwrite/pull/10804) +* Add PostgreSQL database adapter in [#9772](https://github.com/appwrite/appwrite/pull/9772) and [#11293](https://github.com/appwrite/appwrite/pull/11293) +* Add MongoDB support in [#11312](https://github.com/appwrite/appwrite/pull/11312) +* Add new webhooks API in [#11033](https://github.com/appwrite/appwrite/pull/11033) and [#11566](https://github.com/appwrite/appwrite/pull/11566) +* Add schedules API endpoints in [#11331](https://github.com/appwrite/appwrite/pull/11331) +* Add project labels in [#11056](https://github.com/appwrite/appwrite/pull/11056) and project status attribute in [#11291](https://github.com/appwrite/appwrite/pull/11291) +* Add resource-based API key structure in [#11003](https://github.com/appwrite/appwrite/pull/11003) with custom ID support in [#11277](https://github.com/appwrite/appwrite/pull/11277) and list queries in [#11278](https://github.com/appwrite/appwrite/pull/11278) +* Add string types (varchar, text, mediumtext, longtext) for attributes in [#11174](https://github.com/appwrite/appwrite/pull/11174) +* Add encrypt parameter to string attribute types in [#11334](https://github.com/appwrite/appwrite/pull/11334) +* Add int64 format support for integer attributes in [#11123](https://github.com/appwrite/appwrite/pull/11123) +* Add collection and row storage size in [#11254](https://github.com/appwrite/appwrite/pull/11254) and [#11069](https://github.com/appwrite/appwrite/pull/11069) +* Add totalSize on list responses in [#11102](https://github.com/appwrite/appwrite/pull/11102) +* Add custom start command for sites and functions in [#10842](https://github.com/appwrite/appwrite/pull/10842) +* Add separate build/runtime specifications in [#10849](https://github.com/appwrite/appwrite/pull/10849) +* Add deployment retention for sites and functions in [#10959](https://github.com/appwrite/appwrite/pull/10959) +* Add auto-delete old deployments in [#10959](https://github.com/appwrite/appwrite/pull/10959) +* Add custom JWT duration in [#11009](https://github.com/appwrite/appwrite/pull/11009) +* Add multiple application domains support in [#10911](https://github.com/appwrite/appwrite/pull/10911) +* Add GraphQL introspection in [#11159](https://github.com/appwrite/appwrite/pull/11159) +* Add realtime query subscriptions in [#11202](https://github.com/appwrite/appwrite/pull/11202) and [#11237](https://github.com/appwrite/appwrite/pull/11237) +* Add realtime metrics for connections, messages, and bandwidth in [#11438](https://github.com/appwrite/appwrite/pull/11438) and [#11488](https://github.com/appwrite/appwrite/pull/11488) +* Add messaging resource migration support in [#11495](https://github.com/appwrite/appwrite/pull/11495) +* Add cached documents list in [#10832](https://github.com/appwrite/appwrite/pull/10832) +* Add project queries support in [#10990](https://github.com/appwrite/appwrite/pull/10990) +* Add batch document creation in [#10894](https://github.com/appwrite/appwrite/pull/10894) +* Add async screenshots in [#11110](https://github.com/appwrite/appwrite/pull/11110) +* Add new file parameters (encryption, compression) in [#11135](https://github.com/appwrite/appwrite/pull/11135) +* Add new site templates in [#10031](https://github.com/appwrite/appwrite/pull/10031) +* Add VCS repository authorized field in [#11421](https://github.com/appwrite/appwrite/pull/11421) +* Add trusted console projects in [#11248](https://github.com/appwrite/appwrite/pull/11248) + +### Refactoring + +* Refactor to Utopia Platform modules architecture in [#11035](https://github.com/appwrite/appwrite/pull/11035), [#11049](https://github.com/appwrite/appwrite/pull/11049), [#11057](https://github.com/appwrite/appwrite/pull/11057), [#11103](https://github.com/appwrite/appwrite/pull/11103), [#11208](https://github.com/appwrite/appwrite/pull/11208), and [#11398](https://github.com/appwrite/appwrite/pull/11398) +* Refactor auth to single instance in [#10872](https://github.com/appwrite/appwrite/pull/10872) and [#11130](https://github.com/appwrite/appwrite/pull/11130) +* Refactor usage metrics to stateless publisher pattern in [#11449](https://github.com/appwrite/appwrite/pull/11449) +* Refactor messaging and queue in [#10961](https://github.com/appwrite/appwrite/pull/10961) +* Refactor functions schedule in [#10913](https://github.com/appwrite/appwrite/pull/10913) +* Refactor make Bus dispatch synchronous in [#11449](https://github.com/appwrite/appwrite/pull/11449) +* Remove proxy container in [#11039](https://github.com/appwrite/appwrite/pull/11039) + +### Performance + +* Optimize updateDocument() calls to use sparse documents in [#11465](https://github.com/appwrite/appwrite/pull/11465) +* Optimize Dockerfile in [#10947](https://github.com/appwrite/appwrite/pull/10947) +* Improve domain caching in [#11346](https://github.com/appwrite/appwrite/pull/11346) +* Improve memory usage in [#11345](https://github.com/appwrite/appwrite/pull/11345) +* Fix memory leak in [#11067](https://github.com/appwrite/appwrite/pull/11067) and [#11241](https://github.com/appwrite/appwrite/pull/11241) +* Improve realtime performance in [#11251](https://github.com/appwrite/appwrite/pull/11251) +* Enable SMTP keep-alive to reuse connections across mail jobs in [#11496](https://github.com/appwrite/appwrite/pull/11496) ### Fixes -* Fix duplicate document error while creating file in [#10891](https://github.com/appwrite/appwrite/pull/10891) -* Fix "Update external deployment (authorize)" throwing 500 error due to invalid query in [#10888](https://github.com/appwrite/appwrite/pull/10888) -* Fix error setting user password in [#10889](https://github.com/appwrite/appwrite/pull/10889) -* Fix error generating email MFA challenges in [#10884](https://github.com/appwrite/appwrite/pull/10884) -* Fix file token expiry in [#10877](https://github.com/appwrite/appwrite/pull/10877) -* Fix TanStack Nitro default in [#10860](https://github.com/appwrite/appwrite/pull/10860) -* Fix TanStack builds in [#10767](https://github.com/appwrite/appwrite/pull/10767) -* Fix nullable validation in [#10819](https://github.com/appwrite/appwrite/pull/10819) and [#10778](https://github.com/appwrite/appwrite/pull/10778) -* Fix WebP library in [#10738](https://github.com/appwrite/appwrite/pull/10738) -* Fix batch writes in [#10812](https://github.com/appwrite/appwrite/pull/10812) -* Fix error handler error in [#10719](https://github.com/appwrite/appwrite/pull/10719) -* Fix Next 16 compatibility in [#10713](https://github.com/appwrite/appwrite/pull/10713) -* Fix stats usage memory leak in [#10683](https://github.com/appwrite/appwrite/pull/10683) -* Fix author URL in template deployments in [#10535](https://github.com/appwrite/appwrite/pull/10535) -* Fix VCS lock deletion in [#10691](https://github.com/appwrite/appwrite/pull/10691) +* Fix blocked user/resource errors from 401 to 403 in [#11469](https://github.com/appwrite/appwrite/pull/11469) +* Fix OAuth for custom domains in [#10967](https://github.com/appwrite/appwrite/pull/10967) and [#11269](https://github.com/appwrite/appwrite/pull/11269) +* Fix OAuth redirect custom scheme in [#11292](https://github.com/appwrite/appwrite/pull/11292) +* Fix OAuth verified emails in [#10986](https://github.com/appwrite/appwrite/pull/10986) +* Fix MFA recovery code validation in [#10925](https://github.com/appwrite/appwrite/pull/10925) +* Fix users allow updating phone number to empty in [#11521](https://github.com/appwrite/appwrite/pull/11521) +* Fix users optional name error in [#11413](https://github.com/appwrite/appwrite/pull/11413) +* Fix file permissions in [#11026](https://github.com/appwrite/appwrite/pull/11026) +* Fix bulk insert webhook validation in [#11022](https://github.com/appwrite/appwrite/pull/11022) +* Fix execution status update in [#11134](https://github.com/appwrite/appwrite/pull/11134) +* Fix execution timeout status in [#11400](https://github.com/appwrite/appwrite/pull/11400) +* Fix CORS wildcard in [#10956](https://github.com/appwrite/appwrite/pull/10956) +* Fix preflight requests in [#10943](https://github.com/appwrite/appwrite/pull/10943) +* Fix SMTP auth check in [#10939](https://github.com/appwrite/appwrite/pull/10939) +* Fix scheduled executions trigger in [#10922](https://github.com/appwrite/appwrite/pull/10922) +* Fix schedule executions bug in [#10916](https://github.com/appwrite/appwrite/pull/10916) +* Fix deployment enum missing canceled value in [#11179](https://github.com/appwrite/appwrite/pull/11179) +* Fix invalid chunk total in [#11270](https://github.com/appwrite/appwrite/pull/11270) +* Fix sites domains in [#11240](https://github.com/appwrite/appwrite/pull/11240) and [#11355](https://github.com/appwrite/appwrite/pull/11355) +* Fix rule domains in [#11355](https://github.com/appwrite/appwrite/pull/11355) and [#11276](https://github.com/appwrite/appwrite/pull/11276) +* Fix rules deletion in [#11575](https://github.com/appwrite/appwrite/pull/11575) +* Fix VCS template flow in [#11275](https://github.com/appwrite/appwrite/pull/11275) +* Fix VCS comment empty in [#11490](https://github.com/appwrite/appwrite/pull/11490) +* Fix DSN VCS error in [#11364](https://github.com/appwrite/appwrite/pull/11364) +* Fix email URL params encoding in [#11369](https://github.com/appwrite/appwrite/pull/11369) +* Fix missing email warning in [#11378](https://github.com/appwrite/appwrite/pull/11378) +* Fix race condition in builds worker in [#11336](https://github.com/appwrite/appwrite/pull/11336) +* Fix realtime regions in [#11414](https://github.com/appwrite/appwrite/pull/11414) +* Fix realtime errors in [#11573](https://github.com/appwrite/appwrite/pull/11573) +* Fix realtime TablesDB channels in [#11404](https://github.com/appwrite/appwrite/pull/11404) and [#11430](https://github.com/appwrite/appwrite/pull/11430) +* Fix database shared table reconciliation in [#11578](https://github.com/appwrite/appwrite/pull/11578) +* Fix PostgreSQL race condition in shared mode project creation in [#11536](https://github.com/appwrite/appwrite/pull/11536) +* Fix compression enabled env in [#11171](https://github.com/appwrite/appwrite/pull/11171) +* Fix deletes bug in [#10965](https://github.com/appwrite/appwrite/pull/10965) +* Fix devkey scopes in [#10984](https://github.com/appwrite/appwrite/pull/10984) +* Fix phone auth limit in [#11143](https://github.com/appwrite/appwrite/pull/11143) +* Fix relationship document ID validation in [#11193](https://github.com/appwrite/appwrite/pull/11193) +* Fix stale project overwrites OAuth in [#11461](https://github.com/appwrite/appwrite/pull/11461) +* Fix storage health error swallowing in [#11492](https://github.com/appwrite/appwrite/pull/11492) +* Fix Origin validator type error in [#11297](https://github.com/appwrite/appwrite/pull/11297) +* Fix getScreenshot image format in [#11017](https://github.com/appwrite/appwrite/pull/11017) +* Fix migration error handling in [#11457](https://github.com/appwrite/appwrite/pull/11457) +* Fix deprecation warnings in [#11227](https://github.com/appwrite/appwrite/pull/11227) + +### Installer + +* New installer UI in [#11175](https://github.com/appwrite/appwrite/pull/11175) and [#11247](https://github.com/appwrite/appwrite/pull/11247) ### Miscellaneous -* Add CSV export functionality in [#10546](https://github.com/appwrite/appwrite/pull/10546), [#10750](https://github.com/appwrite/appwrite/pull/10750), [#10813](https://github.com/appwrite/appwrite/pull/10813), and [#10847](https://github.com/appwrite/appwrite/pull/10847) -* Add JWT disposition in [#10867](https://github.com/appwrite/appwrite/pull/10867) -* Add screenshots endpoint in [#10675](https://github.com/appwrite/appwrite/pull/10675) -* Add screenshot endpoint stats in [#10706](https://github.com/appwrite/appwrite/pull/10706) -* Add users attributes in [#10688](https://github.com/appwrite/appwrite/pull/10688) -* Add max build duration environment variable in [#10674](https://github.com/appwrite/appwrite/pull/10674) -* Add custom realtime logger in [#10871](https://github.com/appwrite/appwrite/pull/10871) -* Add logs in [#10869](https://github.com/appwrite/appwrite/pull/10869) -* Improve MFA docs endpoint order in [#10793](https://github.com/appwrite/appwrite/pull/10793) -* Auth refactor in [#10758](https://github.com/appwrite/appwrite/pull/10758), [#10837](https://github.com/appwrite/appwrite/pull/10837), [#10682](https://github.com/appwrite/appwrite/pull/10682), and [#10667](https://github.com/appwrite/appwrite/pull/10667) -* Bump assistant to 0.8.4 in [#10887](https://github.com/appwrite/appwrite/pull/10887) -* Bump database to 3.1.5 in [#10766](https://github.com/appwrite/appwrite/pull/10766) -* Bump Utopia DNS in [#10761](https://github.com/appwrite/appwrite/pull/10761) -* Update domains to 0.8.3 in [#10658](https://github.com/appwrite/appwrite/pull/10658) -* Update domains to 0.9.1 in [#10678](https://github.com/appwrite/appwrite/pull/10678) -* Update Apple Swift to 13.3.0 in [#10679](https://github.com/appwrite/appwrite/pull/10679) -* Update Apple Swift in [#10663](https://github.com/appwrite/appwrite/pull/10663) -* Update CLI to 10.2.2 in [#10672](https://github.com/appwrite/appwrite/pull/10672) -* Update to CLI 12.0.0 in [#10853](https://github.com/appwrite/appwrite/pull/10853) -* Update docs examples to use Permission class in [#10707](https://github.com/appwrite/appwrite/pull/10707) -* Update SDK examples docs in [#10855](https://github.com/appwrite/appwrite/pull/10855) -* Release Python SDK in [#10762](https://github.com/appwrite/appwrite/pull/10762) -* Release Flutter 20.3.2 in [#10838](https://github.com/appwrite/appwrite/pull/10838) -* Release Flutter/Dart add screenshot examples in [#10811](https://github.com/appwrite/appwrite/pull/10811) -* Release PHP CLI in [#10791](https://github.com/appwrite/appwrite/pull/10791) -* Release SDKs in [#10817](https://github.com/appwrite/appwrite/pull/10817) -* Update SDKs in [#10694](https://github.com/appwrite/appwrite/pull/10694), [#10729](https://github.com/appwrite/appwrite/pull/10729), and [#10744](https://github.com/appwrite/appwrite/pull/10744) -* Update SDK generator in [#10743](https://github.com/appwrite/appwrite/pull/10743) -* Update database in [#10664](https://github.com/appwrite/appwrite/pull/10664) -* Update README file in [#10763](https://github.com/appwrite/appwrite/pull/10763) -* SDK release documentation in [#10745](https://github.com/appwrite/appwrite/pull/10745) -* SDK release runtime config in [#10765](https://github.com/appwrite/appwrite/pull/10765) -* Sync specs in [#10789](https://github.com/appwrite/appwrite/pull/10789) -* Sync 1.8.0 in [#10677](https://github.com/appwrite/appwrite/pull/10677) -* Add workflow for issue triage in [#10718](https://github.com/appwrite/appwrite/pull/10718) -* Add issue auto-labeler in [#10700](https://github.com/appwrite/appwrite/pull/10700) -* Add AI moderator repo in [#10717](https://github.com/appwrite/appwrite/pull/10717) -* Browser bump in [#10850](https://github.com/appwrite/appwrite/pull/10850) -* Template type enum override in [#10848](https://github.com/appwrite/appwrite/pull/10848) -* VCS reference type in [#10852](https://github.com/appwrite/appwrite/pull/10852) -* Index scope description in [#10851](https://github.com/appwrite/appwrite/pull/10851) -* Config for environment in [#10833](https://github.com/appwrite/appwrite/pull/10833) -* Format instance in [#10830](https://github.com/appwrite/appwrite/pull/10830) -* Replace sleep in webhooks service in [#10656](https://github.com/appwrite/appwrite/pull/10656) -* Update email composer in [#10720](https://github.com/appwrite/appwrite/pull/10720) -* Update facts on GitHub sites and functions in [#10593](https://github.com/appwrite/appwrite/pull/10593) and [#10771](https://github.com/appwrite/appwrite/pull/10771) -* Fix wrong user type in [#10875](https://github.com/appwrite/appwrite/pull/10875) -* Fix limit and offset computation in [#10880](https://github.com/appwrite/appwrite/pull/10880) -* Fix enum examples in [#10828](https://github.com/appwrite/appwrite/pull/10828) -* Fix response models multi-methods in [#10815](https://github.com/appwrite/appwrite/pull/10815) -* Fix undefined variable in [#10654](https://github.com/appwrite/appwrite/pull/10654) -* Fix undefined sequence in [#10652](https://github.com/appwrite/appwrite/pull/10652) -* Fix description in [#10702](https://github.com/appwrite/appwrite/pull/10702) -* Fix warning in builds worker in [#10705](https://github.com/appwrite/appwrite/pull/10705) -* Fix sites create deployment docs in [#10566](https://github.com/appwrite/appwrite/pull/10566) -* Fix test dependencies projects in [#10655](https://github.com/appwrite/appwrite/pull/10655) -* Fix list sites test in [#10726](https://github.com/appwrite/appwrite/pull/10726) +* Add audits upgrade in [#10953](https://github.com/appwrite/appwrite/pull/10953) +* Add graceful workers shutdown in [#11104](https://github.com/appwrite/appwrite/pull/11104) +* Add pool resilience in [#11139](https://github.com/appwrite/appwrite/pull/11139) +* Add function queue job TTL in [#11226](https://github.com/appwrite/appwrite/pull/11226) +* Add cleanup stale executions in [#11146](https://github.com/appwrite/appwrite/pull/11146) +* Add success abuse reset in [#11085](https://github.com/appwrite/appwrite/pull/11085) +* Add SMTP connection validation in [#11079](https://github.com/appwrite/appwrite/pull/11079) +* Add allow custom email sender in [#10945](https://github.com/appwrite/appwrite/pull/10945) +* Add array domains env support in [#11213](https://github.com/appwrite/appwrite/pull/11213) +* Add file create after success hook in [#11054](https://github.com/appwrite/appwrite/pull/11054) +* Add delete subscribers in [#11115](https://github.com/appwrite/appwrite/pull/11115) +* Add cursor plugin in [#11371](https://github.com/appwrite/appwrite/pull/11371) +* Add observability spans in [#11320](https://github.com/appwrite/appwrite/pull/11320), [#11306](https://github.com/appwrite/appwrite/pull/11306), and [#11228](https://github.com/appwrite/appwrite/pull/11228) +* Upgrade PHPStan to v2 with full codebase coverage in [#11550](https://github.com/appwrite/appwrite/pull/11550) +* Upgrade Traefik in [#11265](https://github.com/appwrite/appwrite/pull/11265) +* Upgrade utopia-php/queue in [#11239](https://github.com/appwrite/appwrite/pull/11239) +* Upgrade spomky-labs/otphp in [#11263](https://github.com/appwrite/appwrite/pull/11263) +* Bump utopia-php/database to stable 5.3.15 in [#11573](https://github.com/appwrite/appwrite/pull/11573) +* Bump utopia-php/migration to 1.6.3 in [#11443](https://github.com/appwrite/appwrite/pull/11443) +* Consolidate CI workflows in [#11531](https://github.com/appwrite/appwrite/pull/11531) and [#11551](https://github.com/appwrite/appwrite/pull/11551) +* Hide deprecated methods from docs in [#10933](https://github.com/appwrite/appwrite/pull/10933) +* Deprecate project-level attributes in [#11203](https://github.com/appwrite/appwrite/pull/11203) # Version 1.8.0 diff --git a/README-CN.md b/README-CN.md index afd7eca289..212b5bb08d 100644 --- a/README-CN.md +++ b/README-CN.md @@ -72,7 +72,7 @@ docker run -it --rm \ --volume /var/run/docker.sock:/var/run/docker.sock \ --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \ --entrypoint="install" \ - appwrite/appwrite:1.8.1 + appwrite/appwrite:1.9.0 ``` ### Windows @@ -84,7 +84,7 @@ docker run -it --rm ^ --volume //var/run/docker.sock:/var/run/docker.sock ^ --volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^ --entrypoint="install" ^ - appwrite/appwrite:1.8.1 + appwrite/appwrite:1.9.0 ``` #### PowerShell @@ -94,7 +94,7 @@ docker run -it --rm ` --volume /var/run/docker.sock:/var/run/docker.sock ` --volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ` --entrypoint="install" ` - appwrite/appwrite:1.8.1 + appwrite/appwrite:1.9.0 ``` 运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。 diff --git a/README.md b/README.md index 4a71579207..457863d236 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ docker run -it --rm \ --volume /var/run/docker.sock:/var/run/docker.sock \ --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \ --entrypoint="install" \ - appwrite/appwrite:1.8.1 + appwrite/appwrite:1.9.0 ``` ### Windows @@ -87,7 +87,7 @@ docker run -it --rm ^ --volume //var/run/docker.sock:/var/run/docker.sock ^ --volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^ --entrypoint="install" ^ - appwrite/appwrite:1.8.1 + appwrite/appwrite:1.9.0 ``` #### PowerShell @@ -97,7 +97,7 @@ docker run -it --rm ` --volume /var/run/docker.sock:/var/run/docker.sock ` --volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ` --entrypoint="install" ` - appwrite/appwrite:1.8.1 + appwrite/appwrite:1.9.0 ``` Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation. diff --git a/app/config/collections/common.php b/app/config/collections/common.php index 1845ef8a42..80bb717423 100644 --- a/app/config/collections/common.php +++ b/app/config/collections/common.php @@ -419,6 +419,17 @@ return [ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('impersonator'), + 'type' => Database::VAR_BOOLEAN, + 'signed' => true, + 'size' => 0, + 'format' => '', + 'filters' => [], + 'required' => false, + 'default' => false, + 'array' => false, + ], ], 'indexes' => [ [ @@ -491,6 +502,13 @@ return [ 'lengths' => [], 'orders' => [], ], + [ + '$id' => ID::custom('impersonator'), + 'type' => Database::INDEX_KEY, + 'attributes' => [ID::custom('impersonator')], + 'lengths' => [], + 'orders' => [], + ], ], ], diff --git a/app/config/cors.php b/app/config/cors.php index ef1adeb998..0454a24495 100644 --- a/app/config/cors.php +++ b/app/config/cors.php @@ -28,6 +28,9 @@ return [ 'X-Appwrite-Timestamp', 'X-Appwrite-Session', 'X-Appwrite-Platform', + 'X-Appwrite-Impersonate-User-Id', + 'X-Appwrite-Impersonate-User-Email', + 'X-Appwrite-Impersonate-User-Phone', // SDK generator 'X-SDK-Version', 'X-SDK-Name', diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index a780bfdac3..6d33b45f0b 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -16,7 +16,6 @@ use Appwrite\Event\Mail; use Appwrite\Event\Messaging; use Appwrite\Extend\Exception; use Appwrite\Hooks\Hooks; -use Appwrite\Network\Validator\Email as EmailValidator; use Appwrite\Network\Validator\Redirect; use Appwrite\OpenSSL\OpenSSL; use Appwrite\SDK\AuthType; @@ -60,6 +59,7 @@ use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; use Utopia\Emails\Email; +use Utopia\Emails\Validator\Email as EmailValidator; use Utopia\Http\Http; use Utopia\Locale\Locale; use Utopia\Storage\Validator\FileName; diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index a52ec70b12..1ba5eb1119 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -8,7 +8,6 @@ use Appwrite\Event\Event; use Appwrite\Event\Messaging; use Appwrite\Extend\Exception; use Appwrite\Messaging\Status as MessageStatus; -use Appwrite\Network\Validator\Email; use Appwrite\Permission; use Appwrite\Role; use Appwrite\SDK\AuthType; @@ -43,6 +42,7 @@ use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\Roles; use Utopia\Database\Validator\UID; +use Utopia\Emails\Validator\Email; use Utopia\Http\Http; use Utopia\Locale\Locale; use Utopia\System\System; diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 24a1b28cdd..2fc20ba83f 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -4,10 +4,8 @@ use Ahc\Jwt\JWT; use Appwrite\Auth\Validator\MockNumber; use Appwrite\Event\Delete; use Appwrite\Event\Mail; -use Appwrite\Event\Validator\Event; use Appwrite\Extend\Exception; use Appwrite\Network\Platform; -use Appwrite\Network\Validator\Email; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Deprecated; @@ -30,7 +28,7 @@ use Utopia\Database\Query; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\UID; -use Utopia\Domains\Validator\PublicDomain; +use Utopia\Emails\Validator\Email; use Utopia\Http\Http; use Utopia\Locale\Locale; use Utopia\System\System; @@ -38,11 +36,9 @@ use Utopia\Validator\ArrayList; 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; use Utopia\Validator\WhiteList; Http::init() @@ -773,312 +769,6 @@ Http::delete('/v1/projects/:projectId') $response->noContent(); }); -// Webhooks - -Http::post('/v1/projects/:projectId/webhooks') - ->desc('Create webhook') - ->groups(['api', 'projects']) - ->label('scope', 'projects.write') - ->label('sdk', new Method( - namespace: 'projects', - group: 'webhooks', - name: 'createWebhook', - description: '/docs/references/projects/create-webhook.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_CREATED, - model: Response::MODEL_WEBHOOK, - ) - ] - )) - ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) - ->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.') - ->param('enabled', true, new Boolean(true), 'Enable or disable a webhook.', true) - ->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.') - ->param('url', '', fn ($request) => new Multiple([new URL(['http', 'https']), new PublicDomain()], Multiple::TYPE_STRING), 'Webhook URL.', false, ['request']) - ->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.') - ->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true) - ->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true) - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $projectId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $security = (bool) filter_var($security, FILTER_VALIDATE_BOOLEAN); - - $webhook = new Document([ - '$id' => ID::unique(), - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'projectInternalId' => $project->getSequence(), - 'projectId' => $project->getId(), - 'name' => $name, - 'events' => $events, - 'url' => $url, - 'security' => $security, - 'httpUser' => $httpUser, - 'httpPass' => $httpPass, - 'signatureKey' => \bin2hex(\random_bytes(64)), - 'enabled' => $enabled, - ]); - - $webhook = $dbForPlatform->createDocument('webhooks', $webhook); - - $dbForPlatform->purgeCachedDocument('projects', $project->getId()); - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic($webhook, Response::MODEL_WEBHOOK); - }); - -Http::get('/v1/projects/:projectId/webhooks') - ->desc('List webhooks') - ->groups(['api', 'projects']) - ->label('scope', 'projects.read') - ->label('sdk', new Method( - namespace: 'projects', - group: 'webhooks', - name: 'listWebhooks', - description: '/docs/references/projects/list-webhooks.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_WEBHOOK_LIST, - ) - ] - )) - ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) - ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $webhooks = $dbForPlatform->find('webhooks', [ - Query::equal('projectInternalId', [$project->getSequence()]), - Query::limit(5000), - ]); - - $response->dynamic(new Document([ - 'webhooks' => $webhooks, - 'total' => $includeTotal ? count($webhooks) : 0, - ]), Response::MODEL_WEBHOOK_LIST); - }); - -Http::get('/v1/projects/:projectId/webhooks/:webhookId') - ->desc('Get webhook') - ->groups(['api', 'projects']) - ->label('scope', 'projects.read') - ->label('sdk', new Method( - namespace: 'projects', - group: 'webhooks', - name: 'getWebhook', - description: '/docs/references/projects/get-webhook.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_WEBHOOK, - ) - ] - )) - ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) - ->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook unique ID.', false, ['dbForPlatform']) - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $webhook = $dbForPlatform->findOne('webhooks', [ - Query::equal('$id', [$webhookId]), - Query::equal('projectInternalId', [$project->getSequence()]), - ]); - - if ($webhook->isEmpty()) { - throw new Exception(Exception::WEBHOOK_NOT_FOUND); - } - - $response->dynamic($webhook, Response::MODEL_WEBHOOK); - }); - -Http::put('/v1/projects/:projectId/webhooks/:webhookId') - ->desc('Update webhook') - ->groups(['api', 'projects']) - ->label('scope', 'projects.write') - ->label('sdk', new Method( - namespace: 'projects', - group: 'webhooks', - name: 'updateWebhook', - description: '/docs/references/projects/update-webhook.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_WEBHOOK, - ) - ] - )) - ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) - ->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook unique ID.', false, ['dbForPlatform']) - ->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.') - ->param('enabled', true, new Boolean(true), 'Enable or disable a webhook.', true) - ->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.') - ->param('url', '', fn ($request) => new Multiple([new URL(['http', 'https']), new PublicDomain()], Multiple::TYPE_STRING), 'Webhook URL.', false, ['request']) - ->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.') - ->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true) - ->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true) - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $projectId, string $webhookId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $security = ($security === '1' || $security === 'true' || $security === 1 || $security === true); - - $webhook = $dbForPlatform->findOne('webhooks', [ - Query::equal('$id', [$webhookId]), - Query::equal('projectInternalId', [$project->getSequence()]), - ]); - - if ($webhook->isEmpty()) { - throw new Exception(Exception::WEBHOOK_NOT_FOUND); - } - - $webhook - ->setAttribute('name', $name) - ->setAttribute('events', $events) - ->setAttribute('url', $url) - ->setAttribute('security', $security) - ->setAttribute('httpUser', $httpUser) - ->setAttribute('httpPass', $httpPass) - ->setAttribute('enabled', $enabled); - - if ($enabled) { - $webhook->setAttribute('attempts', 0); - } - - $dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook); - $dbForPlatform->purgeCachedDocument('projects', $project->getId()); - - $response->dynamic($webhook, Response::MODEL_WEBHOOK); - }); - -Http::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') - ->desc('Update webhook signature key') - ->groups(['api', 'projects']) - ->label('scope', 'projects.write') - ->label('sdk', new Method( - namespace: 'projects', - group: 'webhooks', - name: 'updateWebhookSignature', - description: '/docs/references/projects/update-webhook-signature.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_WEBHOOK, - ) - ] - )) - ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) - ->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook unique ID.', false, ['dbForPlatform']) - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $webhook = $dbForPlatform->findOne('webhooks', [ - Query::equal('$id', [$webhookId]), - Query::equal('projectInternalId', [$project->getSequence()]), - ]); - - if ($webhook->isEmpty()) { - throw new Exception(Exception::WEBHOOK_NOT_FOUND); - } - - $webhook->setAttribute('signatureKey', \bin2hex(\random_bytes(64))); - - $dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook); - $dbForPlatform->purgeCachedDocument('projects', $project->getId()); - - $response->dynamic($webhook, Response::MODEL_WEBHOOK); - }); - -Http::delete('/v1/projects/:projectId/webhooks/:webhookId') - ->desc('Delete webhook') - ->groups(['api', 'projects']) - ->label('scope', 'projects.write') - ->label('sdk', new Method( - namespace: 'projects', - group: 'webhooks', - name: 'deleteWebhook', - description: '/docs/references/projects/delete-webhook.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_NOCONTENT, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::NONE - )) - ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) - ->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook unique ID.', false, ['dbForPlatform']) - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $webhook = $dbForPlatform->findOne('webhooks', [ - Query::equal('$id', [$webhookId]), - Query::equal('projectInternalId', [$project->getSequence()]), - ]); - - if ($webhook->isEmpty()) { - throw new Exception(Exception::WEBHOOK_NOT_FOUND); - } - - $dbForPlatform->deleteDocument('webhooks', $webhook->getId()); - - $dbForPlatform->purgeCachedDocument('projects', $project->getId()); - - $response->noContent(); - }); - // Keys Http::post('/v1/projects/:projectId/keys') diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 9d04018b10..3b21d4797d 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -15,7 +15,6 @@ use Appwrite\Event\Delete; use Appwrite\Event\Event; use Appwrite\Extend\Exception; use Appwrite\Hooks\Hooks; -use Appwrite\Network\Validator\Email as EmailValidator; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Deprecated; @@ -60,6 +59,7 @@ use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; use Utopia\Emails\Email; +use Utopia\Emails\Validator\Email as EmailValidator; use Utopia\Http\Http; use Utopia\Locale\Locale; use Utopia\System\System; @@ -1212,6 +1212,47 @@ Http::put('/v1/users/:userId/labels') $response->dynamic($user, Response::MODEL_USER); }); +Http::patch('/v1/users/:userId/impersonator') + ->desc('Update user impersonator capability') + ->groups(['api', 'users']) + ->label('event', 'users.[userId].update.impersonator') + ->label('scope', 'users.write') + ->label('audits.event', 'user.update') + ->label('audits.resource', 'user/{response.$id}') + ->label('sdk', new Method( + namespace: 'users', + group: 'users', + name: 'updateImpersonator', + description: '/docs/references/users/update-user-impersonator.md', + auth: [AuthType::ADMIN, AuthType::KEY], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_USER, + ) + ] + )) + ->param('userId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'User ID.', false, ['dbForProject']) + ->param('impersonator', false, new Boolean(true), 'Whether the user can impersonate other users. When true, the user can browse project users to choose a target and can pass impersonation headers to act as that user. Internal audit logs still attribute impersonated actions to the original impersonator and store the target user details only in internal audit payload data.') + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->action(function (string $userId, bool $impersonator, Response $response, Database $dbForProject, Event $queueForEvents) { + + $user = $dbForProject->getDocument('users', $userId); + + if ($user->isEmpty()) { + throw new Exception(Exception::USER_NOT_FOUND); + } + + $user = $dbForProject->updateDocument('users', $user->getId(), new Document(['impersonator' => $impersonator])); + + $queueForEvents + ->setParam('userId', $user->getId()); + + $response->dynamic($user, Response::MODEL_USER); + }); + Http::patch('/v1/users/:userId/verification/phone') ->desc('Update phone verification') ->groups(['api', 'users']) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 94a44da016..974300f4c3 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -345,6 +345,19 @@ Http::init() $scopes = \array_unique($scopes); + // Intentional: impersonators get users.read so they can discover a target user + // before impersonation starts, and keep that access while impersonating. + if ( + !$user->isEmpty() + && ( + $user->getAttribute('impersonator', false) + || $user->getAttribute('impersonatorUserId') + ) + ) { + $scopes[] = 'users.read'; + $scopes = \array_unique($scopes); + } + $authorization->addRole($role); foreach ($user->getRoles($authorization) as $authRole) { $authorization->addRole($authRole); @@ -376,8 +389,11 @@ Http::init() } if (! empty($user->getId())) { + $impersonatorUserId = $user->getAttribute('impersonatorUserId'); $accessedAt = $user->getAttribute('accessedAt', 0); - if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) { + + // Skip updating accessedAt for impersonated requests so we don't attribute activity to the target user. + if (! $impersonatorUserId && DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) { $user->setAttribute('accessedAt', DateTime::now()); if ($project->getId() !== 'console' && $mode !== APP_MODE_ADMIN) { diff --git a/app/init/constants.php b/app/init/constants.php index 895b202099..9d524a7acd 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -47,7 +47,7 @@ const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours const APP_CACHE_BUSTER = 4321; -const APP_VERSION_STABLE = '1.8.1'; +const APP_VERSION_STABLE = '1.9.0'; const APP_DATABASE_ATTRIBUTE_EMAIL = 'email'; const APP_DATABASE_ATTRIBUTE_ENUM = 'enum'; const APP_DATABASE_ATTRIBUTE_IP = 'ip'; diff --git a/app/init/database/formats.php b/app/init/database/formats.php index 6c73877576..29a4f0c7d4 100644 --- a/app/init/database/formats.php +++ b/app/init/database/formats.php @@ -1,9 +1,9 @@ getHeader('x-appwrite-impersonate-user-id', ''); + $impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', ''); + $impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', ''); + if (!$user->isEmpty() && $user->getAttribute('impersonator', false)) { + $userDb = (APP_MODE_ADMIN === $mode || $project->getId() === 'console') ? $dbForPlatform : $dbForProject; + $targetUser = null; + if (!empty($impersonateUserId)) { + $targetUser = $userDb->getAuthorization()->skip(fn () => $userDb->getDocument('users', $impersonateUserId)); + } elseif (!empty($impersonateEmail)) { + $targetUser = $userDb->getAuthorization()->skip(fn () => $userDb->findOne('users', [Query::equal('email', [\strtolower($impersonateEmail)])])); + } elseif (!empty($impersonatePhone)) { + $targetUser = $userDb->getAuthorization()->skip(fn () => $userDb->findOne('users', [Query::equal('phone', [$impersonatePhone])])); + } + if ($targetUser !== null && !$targetUser->isEmpty()) { + $impersonator = clone $user; + $user = clone $targetUser; + $user->setAttribute('impersonatorUserId', $impersonator->getId()); + $user->setAttribute('impersonatorUserInternalId', $impersonator->getSequence()); + $user->setAttribute('impersonatorUserName', $impersonator->getAttribute('name', '')); + $user->setAttribute('impersonatorUserEmail', $impersonator->getAttribute('email', '')); + $user->setAttribute('impersonatorAccessedAt', $impersonator->getAttribute('accessedAt', 0)); + } + } + $dbForProject->setMetadata('user', $user->getId()); $dbForPlatform->setMetadata('user', $user->getId()); return $user; }, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']); -Http::setResource('project', function ($dbForPlatform, $request, $console, $authorization) { +Http::setResource('project', function ($dbForPlatform, $request, $console, $authorization, Http $utopia) { /** @var Appwrite\Utopia\Request $request */ /** @var Utopia\Database\Database $dbForPlatform */ /** @var Utopia\Database\Document $console */ @@ -486,6 +511,20 @@ Http::setResource('project', function ($dbForPlatform, $request, $console, $auth $projectId = $request->getHeader('x-appwrite-project', ''); } + // Backwards compatibility for new services, originally project resources + // These endpoints moved from /v1/projects/:projectId/ to /v1/ + // When accessed via the old alias path, extract projectId from the URI + $deprecatedProjectPathPrefix = '/v1/projects/'; + $route = $utopia->match($request); + if (!empty($route)) { + $isDeprecatedAlias = \str_starts_with($request->getURI(), $deprecatedProjectPathPrefix) && + !\str_starts_with($route->getPath(), $deprecatedProjectPathPrefix); + + if ($isDeprecatedAlias) { + $projectId = \explode('/', $request->getURI(), 5)[3] ?? ''; + } + } + if (empty($projectId) || $projectId === 'console') { return $console; } @@ -493,7 +532,7 @@ Http::setResource('project', function ($dbForPlatform, $request, $console, $auth $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); return $project; -}, ['dbForPlatform', 'request', 'console', 'authorization']); +}, ['dbForPlatform', 'request', 'console', 'authorization', 'utopia']); Http::setResource('session', function (User $user, Store $store, Token $proofForToken) { if ($user->isEmpty()) { @@ -1072,16 +1111,21 @@ function getDevice(string $root, string $connection = ''): Device } } -Http::setResource('mode', function ($request) { - /** @var Appwrite\Utopia\Request $request */ - +Http::setResource('mode', function (Request $request, Document $project) { /** * Defines the mode for the request: * - 'default' => Requests for Client and Server Side * - 'admin' => Request from the Console on non-console projects */ - return $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT)); -}, ['request']); + $mode = $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT)); + + $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', '')); + if (!empty($projectId) && $project->getId() !== $projectId) { + $mode = APP_MODE_ADMIN; + } + + return $mode; +}, ['request', 'project']); Http::setResource('geodb', function ($register) { /** @var Utopia\Registry\Registry $register */ diff --git a/app/views/install/installer.phtml b/app/views/install/installer.phtml index 3cf2b77aa9..05bc1b80ed 100644 --- a/app/views/install/installer.phtml +++ b/app/views/install/installer.phtml @@ -9,6 +9,7 @@ $defaultAppDomain = ($defaultAppDomain === 'traefik') ? 'localhost' : $defaultAp $defaultEmailCertificates = $defaultEmailCertificates ?? ''; $defaultDatabase = $vars['_APP_DB_ADAPTER']['default'] ?? 'mongodb'; $lockedDatabase = $isUpgrade && empty($lockedDatabase) ? $defaultDatabase : $lockedDatabase; +$enabledDatabases = $enabledDatabases ?? ['mongodb', 'mariadb', 'postgresql']; $isLocalInstall = $isLocalInstall ?? false; @@ -64,6 +65,7 @@ $installerVersion = @filemtime(__DIR__ . '/installer/js/installer.js') ?: time() data-default-secret-key="" data-default-assistant-openai-key="" data-default-database="" + data-enabled-databases="" data-dev-mode="true" diff --git a/app/views/install/installer/css/styles.css b/app/views/install/installer/css/styles.css index 4601cfcb67..b1d8fe5089 100644 --- a/app/views/install/installer/css/styles.css +++ b/app/views/install/installer/css/styles.css @@ -11,9 +11,13 @@ --neutral-300: rgba(173, 173, 176, 1); --neutral-400: rgba(151, 151, 155, 1); --neutral-500: rgba(129, 129, 134, 1); + --neutral-600: rgba(108, 108, 113, 1); --neutral-700: rgba(86, 86, 92, 1); + --neutral-750: rgba(65, 65, 70, 1); --neutral-800: rgba(45, 45, 49, 1); + --neutral-850: rgba(29, 29, 33, 1); --neutral-900: rgba(25, 25, 28, 1); + --neutral-250: rgba(195, 195, 198, 1); /* Warning colors */ --web-orange-200: rgba(255, 213, 194, 1); @@ -172,6 +176,44 @@ --fgColor-neutral-weak: var(--fgcolor-neutral-weak); --fgColor-accent: var(--fgcolor-accent); --fgColor-on-accent: var(--fgcolor-on-accent); + + color-scheme: light dark; +} + +@media (prefers-color-scheme: dark) { + :root { + --bgcolor-neutral-default: var(--neutral-900); + --bgcolor-neutral-primary: var(--neutral-850); + --bgcolor-neutral-secondary: var(--neutral-800); + --bgcolor-neutral-tertiary: var(--neutral-800); + --bgcolor-neutral-invert-weaker: var(--neutral-400); + --bgcolor-neutral-invert-weak: var(--neutral-300); + --bgcolor-success-weak: rgba(16, 185, 129, 0.12); + --bgcolor-warning-weaker: rgba(254, 124, 67, 0.08); + --bgcolor-warning-weak: rgba(254, 124, 67, 0.12); + --bgcolor-error-weaker: rgba(255, 69, 58, 0.08); + + --fgcolor-neutral-primary: var(--neutral-50); + --fgcolor-neutral-secondary: var(--neutral-250); + --fgcolor-neutral-tertiary: var(--neutral-500); + --fgcolor-neutral-weak: var(--neutral-600); + --fgcolor-on-accent: var(--neutral-0); + --fgcolor-on-invert: var(--neutral-900); + --fgcolor-on-success-weak: rgba(52, 211, 153, 1); + --fgcolor-warning: var(--web-orange-500); + --fgcolor-on-warning-weak: var(--web-orange-500); + --fgcolor-error: var(--web-red-500); + --fgcolor-on-error: var(--neutral-0); + + --border-neutral: var(--neutral-800); + --border-neutral-strong: var(--neutral-750); + --border-neutral-stronger: var(--neutral-600); + --border-focus: var(--neutral-600); + + --overlay-neutral-hover: rgba(255, 255, 255, 0.04); + --overlay-neutral-pressed: rgba(255, 255, 255, 0.08); + --overlay-scrim: rgba(0, 0, 0, 0.48); + } } .installer-toast-stack { @@ -1354,6 +1396,7 @@ body { align-items: center; justify-content: center; line-height: 0; + color: var(--fgcolor-neutral-weak); } .step-indicator svg { diff --git a/app/views/install/installer/icons/appwrite-logo.svg b/app/views/install/installer/icons/appwrite-logo.svg index f0f2fa5af0..af9378f8fc 100644 --- a/app/views/install/installer/icons/appwrite-logo.svg +++ b/app/views/install/installer/icons/appwrite-logo.svg @@ -1,13 +1,13 @@ diff --git a/app/views/install/installer/icons/indicator-inactive.svg b/app/views/install/installer/icons/indicator-inactive.svg index 78b5ef3ac0..550101da9b 100644 --- a/app/views/install/installer/icons/indicator-inactive.svg +++ b/app/views/install/installer/icons/indicator-inactive.svg @@ -1,3 +1,3 @@ diff --git a/app/views/install/installer/js/modules/context.js b/app/views/install/installer/js/modules/context.js index bc08a8c8df..c531ecddce 100644 --- a/app/views/install/installer/js/modules/context.js +++ b/app/views/install/installer/js/modules/context.js @@ -2,6 +2,11 @@ const getBodyDataset = () => document.body?.dataset ?? {}; const isUpgradeMode = () => getBodyDataset().upgrade === 'true'; const getLockedDatabase = () => getBodyDataset().lockedDatabase || ''; + const getEnabledDatabases = () => { + const raw = getBodyDataset().enabledDatabases; + if (!raw) return ['mongodb', 'mariadb', 'postgresql']; + try { return JSON.parse(raw); } catch (e) { return ['mongodb', 'mariadb', 'postgresql']; } + }; const STEP_IDS = Object.freeze({ CONFIG_FILES: 'config-files', @@ -95,6 +100,7 @@ getBodyDataset, isUpgradeMode, getLockedDatabase, + getEnabledDatabases, STEP_IDS, STATUS, SSE_EVENTS, diff --git a/app/views/install/installer/js/modules/progress.js b/app/views/install/installer/js/modules/progress.js index a6e784ada9..7f7b23e3fc 100644 --- a/app/views/install/installer/js/modules/progress.js +++ b/app/views/install/installer/js/modules/progress.js @@ -27,6 +27,7 @@ let activeInstall = null; let unloadGuard = null; + let sseSessionDetails = null; const csrfToken = document.querySelector('meta[name="appwrite-installer-csrf"]')?.getAttribute('content') || ''; const withCsrfHeader = (headers = {}) => { @@ -524,6 +525,9 @@ inProgress: payload.message || payload.step, done: payload.message || payload.step }; + if (step.id === STEP_IDS.ACCOUNT_SETUP && payload.details?.sessionSecret) { + sseSessionDetails = payload.details; + } progressState.set(step.id, { status: payload.status || STATUS.IN_PROGRESS, message: payload.message, @@ -599,7 +603,7 @@ }); if (!allDone) return; const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP); - const sessionDetails = accountState?.details; + const sessionDetails = sseSessionDetails || accountState?.details; finalizeInstall(); notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => { setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0); @@ -740,7 +744,7 @@ } const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP); - const sessionDetails = accountState?.details; + const sessionDetails = sseSessionDetails || accountState?.details; finalizeInstall(); notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => { setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0); diff --git a/app/views/install/installer/js/steps.js b/app/views/install/installer/js/steps.js index d8b0621761..2a71d075cc 100644 --- a/app/views/install/installer/js/steps.js +++ b/app/views/install/installer/js/steps.js @@ -9,7 +9,8 @@ const { INSTALLATION_STEPS, clampStep, - isUpgradeMode + isUpgradeMode, + getEnabledDatabases } = Context; const { @@ -79,6 +80,19 @@ } }; + const applyEnabledDatabases = (root) => { + const enabled = getEnabledDatabases?.() || []; + const radios = root.querySelectorAll('input[name="database"]'); + radios.forEach((radio) => { + if (!enabled.includes(radio.value)) { + const card = radio.closest('.selector-card'); + if (card) { + card.remove(); + } + } + }); + }; + const bindDatabaseSelection = (root) => { const radios = root.querySelectorAll('input[name="database"]'); radios.forEach((radio) => { @@ -139,6 +153,8 @@ return; } + applyEnabledDatabases(root); + const lockedDatabase = getLockedDatabase?.() || ''; if (lockedDatabase) { lockDatabaseSelection(root, lockedDatabase); diff --git a/app/views/install/installer/templates/steps/step-1.phtml b/app/views/install/installer/templates/steps/step-1.phtml index ae7674c3bf..8f4a726158 100644 --- a/app/views/install/installer/templates/steps/step-1.phtml +++ b/app/views/install/installer/templates/steps/step-1.phtml @@ -7,8 +7,12 @@ $defaultHttpsPort = $defaultHttpsPort ?? '443'; $defaultEmailCertificates = $defaultEmailCertificates ?? ''; $defaultAssistantOpenAIKey = $defaultAssistantOpenAIKey ?? ''; $defaultDatabase = $defaultDatabase ?? 'mongodb'; +$enabledDatabases = $enabledDatabases ?? ['mongodb', 'mariadb', 'postgresql']; $selectedDatabase = $lockedDatabase ?: $defaultDatabase; $isDatabaseLocked = !empty($lockedDatabase); +$mongoEnabled = in_array('mongodb', $enabledDatabases, true); +$mariaEnabled = in_array('mariadb', $enabledDatabases, true); +$postgresEnabled = in_array('postgresql', $enabledDatabases, true); $mongoDisabled = $isDatabaseLocked && $selectedDatabase !== 'mongodb'; $mariaDisabled = $isDatabaseLocked && $selectedDatabase !== 'mariadb'; $postgresDisabled = $isDatabaseLocked && $selectedDatabase !== 'postgresql'; @@ -43,6 +47,7 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
+