mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge branch '1.8.x' into feat-audit-user-type-distinction
This commit is contained in:
+42
-35
@@ -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
|
||||
|
||||
+119
-86
@@ -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
|
||||
|
||||
|
||||
+3
-3
@@ -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 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Database\Validator\Structure;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Validator\IP;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\URL;
|
||||
|
||||
+51
-7
@@ -470,13 +470,38 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
|
||||
}
|
||||
}
|
||||
|
||||
// Impersonation: if current user has impersonator capability and headers are set, act as another user
|
||||
$impersonateUserId = $request->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/<resource> to /v1/<resource>
|
||||
// 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 */
|
||||
|
||||
@@ -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="<?php echo htmlspecialchars((string) $defaultDatabase, ENT_QUOTES, 'UTF-8'); ?>"
|
||||
data-enabled-databases="<?php echo htmlspecialchars(json_encode(array_values($enabledDatabases)), ENT_QUOTES, 'UTF-8'); ?>"
|
||||
<?php if ($isLocalInstall) { ?>
|
||||
data-dev-mode="true"
|
||||
<?php } ?>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<svg width="131" height="25" viewBox="0 0 131 25" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M38.2643 19.5087C40.4069 19.5087 41.49 18.3947 41.9609 17.6362H42.1728C42.267 18.4421 42.832 19.2006 43.9386 19.2006H46.0341V16.8304H45.4926C45.1159 16.8304 44.9275 16.617 44.9275 16.2852V6.78055H42.1492V8.29751H41.9373C41.3958 7.53903 40.2656 6.47242 38.1937 6.47242C34.8974 6.47242 32.4487 9.2219 32.4487 12.9906C32.4487 16.7593 34.9445 19.5087 38.2643 19.5087ZM38.7588 16.8067C36.8045 16.8067 35.2741 15.3608 35.2741 13.0143C35.2741 10.7151 36.7574 9.15079 38.7352 9.15079C40.6188 9.15079 42.1963 10.5492 42.1963 13.0143C42.1963 15.1238 40.8543 16.8067 38.7588 16.8067Z" fill="#19191D"/>
|
||||
<path d="M47.6745 24.0166H50.4528V17.6362H50.6647C51.1826 18.3947 52.2893 19.5087 54.4789 19.5087C57.7752 19.5087 60.1768 16.7118 60.1768 12.9906C60.1768 9.2456 57.6104 6.47242 54.2906 6.47242C52.1715 6.47242 51.1356 7.63384 50.6411 8.2738H50.4292V6.78055H47.6745V24.0166ZM53.8903 16.8778C51.9832 16.8778 50.4057 15.4556 50.4057 12.9906C50.4057 10.8811 51.7477 9.10339 53.8432 9.10339C55.7974 9.10339 57.3279 10.644 57.3279 12.9906C57.3279 15.2897 55.8445 16.8778 53.8903 16.8778Z" fill="#19191D"/>
|
||||
<path d="M61.6104 24.0166H64.3887V17.6362H64.6006C65.1186 18.3947 66.2252 19.5087 68.4149 19.5087C71.7112 19.5087 73.8839 16.7118 73.8839 12.9906C73.8839 9.2456 71.5464 6.47242 68.2265 6.47242C66.1075 6.47242 65.0715 7.63384 64.5771 8.2738H64.3652V6.78055H61.6104V24.0166ZM67.8263 16.8778C65.9191 16.8778 64.3416 15.4556 64.3416 12.9906C64.3416 10.8811 65.6837 9.10339 67.7792 9.10339C69.7334 9.10339 71.2638 10.644 71.2638 12.9906C71.2638 15.2897 69.7805 16.8778 67.8263 16.8778Z" fill="#19191D"/>
|
||||
<path d="M77.5565 19.489H81.4885L83.7252 9.74733H83.8665L86.1033 19.489H90.0117L93.1415 7.06896H90.3414L88.1046 16.8343H87.8927L85.6559 7.06896H81.9594L79.6991 16.8343H79.4872L77.2739 7.06896H74.3073L77.5565 19.489Z" fill="#19191D"/>
|
||||
<path d="M94.549 19.489H97.3273V13.3501C97.3273 11.0036 98.4104 9.55771 100.435 9.55771H101.66V6.76083H100.741C99.1638 6.76083 97.963 7.85114 97.4921 8.89405H97.3038V7.06896H94.549V19.489Z" fill="#19191D"/>
|
||||
<path d="M115.447 19.489H117.613V17.0003H115.47C114.623 17.0003 114.27 16.621 114.27 15.744V9.53401H117.754V7.06896H114.27V3.58472H111.633V7.06896H109.325V9.53401H111.468V15.7677C111.468 18.3987 113.045 19.489 115.447 19.489Z" fill="#19191D"/>
|
||||
<path d="M125.067 19.5087C127.633 19.5087 129.893 18.2288 130.694 15.6452L128.151 15.029C127.704 16.4037 126.409 17.1148 125.043 17.1148C123.018 17.1148 121.676 15.7875 121.653 13.7016H131V12.9195C131 9.2219 128.716 6.47242 124.949 6.47242C121.629 6.47242 118.78 9.10339 118.78 13.0143C118.78 16.8067 121.3 19.5087 125.067 19.5087ZM121.676 11.6632C121.841 10.17 123.183 8.91377 124.949 8.91377C126.644 8.91377 128.033 9.98037 128.175 11.6632H121.676Z" fill="#19191D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M108.09 19.489H105.312V9.53401H103.145V7.06896H108.09V19.489Z" fill="#19191D"/>
|
||||
<path d="M106.494 5.34533C107.507 5.34533 108.26 4.58686 108.26 3.59136C108.26 2.61956 107.507 1.86108 106.494 1.86108C105.482 1.86108 104.729 2.61956 104.729 3.59136C104.729 4.58686 105.482 5.34533 106.494 5.34533Z" fill="#19191D"/>
|
||||
<path d="M38.2643 19.5087C40.4069 19.5087 41.49 18.3947 41.9609 17.6362H42.1728C42.267 18.4421 42.832 19.2006 43.9386 19.2006H46.0341V16.8304H45.4926C45.1159 16.8304 44.9275 16.617 44.9275 16.2852V6.78055H42.1492V8.29751H41.9373C41.3958 7.53903 40.2656 6.47242 38.1937 6.47242C34.8974 6.47242 32.4487 9.2219 32.4487 12.9906C32.4487 16.7593 34.9445 19.5087 38.2643 19.5087ZM38.7588 16.8067C36.8045 16.8067 35.2741 15.3608 35.2741 13.0143C35.2741 10.7151 36.7574 9.15079 38.7352 9.15079C40.6188 9.15079 42.1963 10.5492 42.1963 13.0143C42.1963 15.1238 40.8543 16.8067 38.7588 16.8067Z" fill="currentColor"/>
|
||||
<path d="M47.6745 24.0166H50.4528V17.6362H50.6647C51.1826 18.3947 52.2893 19.5087 54.4789 19.5087C57.7752 19.5087 60.1768 16.7118 60.1768 12.9906C60.1768 9.2456 57.6104 6.47242 54.2906 6.47242C52.1715 6.47242 51.1356 7.63384 50.6411 8.2738H50.4292V6.78055H47.6745V24.0166ZM53.8903 16.8778C51.9832 16.8778 50.4057 15.4556 50.4057 12.9906C50.4057 10.8811 51.7477 9.10339 53.8432 9.10339C55.7974 9.10339 57.3279 10.644 57.3279 12.9906C57.3279 15.2897 55.8445 16.8778 53.8903 16.8778Z" fill="currentColor"/>
|
||||
<path d="M61.6104 24.0166H64.3887V17.6362H64.6006C65.1186 18.3947 66.2252 19.5087 68.4149 19.5087C71.7112 19.5087 73.8839 16.7118 73.8839 12.9906C73.8839 9.2456 71.5464 6.47242 68.2265 6.47242C66.1075 6.47242 65.0715 7.63384 64.5771 8.2738H64.3652V6.78055H61.6104V24.0166ZM67.8263 16.8778C65.9191 16.8778 64.3416 15.4556 64.3416 12.9906C64.3416 10.8811 65.6837 9.10339 67.7792 9.10339C69.7334 9.10339 71.2638 10.644 71.2638 12.9906C71.2638 15.2897 69.7805 16.8778 67.8263 16.8778Z" fill="currentColor"/>
|
||||
<path d="M77.5565 19.489H81.4885L83.7252 9.74733H83.8665L86.1033 19.489H90.0117L93.1415 7.06896H90.3414L88.1046 16.8343H87.8927L85.6559 7.06896H81.9594L79.6991 16.8343H79.4872L77.2739 7.06896H74.3073L77.5565 19.489Z" fill="currentColor"/>
|
||||
<path d="M94.549 19.489H97.3273V13.3501C97.3273 11.0036 98.4104 9.55771 100.435 9.55771H101.66V6.76083H100.741C99.1638 6.76083 97.963 7.85114 97.4921 8.89405H97.3038V7.06896H94.549V19.489Z" fill="currentColor"/>
|
||||
<path d="M115.447 19.489H117.613V17.0003H115.47C114.623 17.0003 114.27 16.621 114.27 15.744V9.53401H117.754V7.06896H114.27V3.58472H111.633V7.06896H109.325V9.53401H111.468V15.7677C111.468 18.3987 113.045 19.489 115.447 19.489Z" fill="currentColor"/>
|
||||
<path d="M125.067 19.5087C127.633 19.5087 129.893 18.2288 130.694 15.6452L128.151 15.029C127.704 16.4037 126.409 17.1148 125.043 17.1148C123.018 17.1148 121.676 15.7875 121.653 13.7016H131V12.9195C131 9.2219 128.716 6.47242 124.949 6.47242C121.629 6.47242 118.78 9.10339 118.78 13.0143C118.78 16.8067 121.3 19.5087 125.067 19.5087ZM121.676 11.6632C121.841 10.17 123.183 8.91377 124.949 8.91377C126.644 8.91377 128.033 9.98037 128.175 11.6632H121.676Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M108.09 19.489H105.312V9.53401H103.145V7.06896H108.09V19.489Z" fill="currentColor"/>
|
||||
<path d="M106.494 5.34533C107.507 5.34533 108.26 4.58686 108.26 3.59136C108.26 2.61956 107.507 1.86108 106.494 1.86108C105.482 1.86108 104.729 2.61956 104.729 3.59136C104.729 4.58686 105.482 5.34533 106.494 5.34533Z" fill="currentColor"/>
|
||||
<path d="M24.2577 16.4436V21.9248H10.6705C6.71194 21.9248 3.25559 19.7204 1.40636 16.4436C1.13754 15.9672 0.90225 15.4674 0.704883 14.9487C0.31744 13.9322 0.0738912 12.8415 0 11.7034V10.2214C0.0160422 9.96781 0.0413207 9.71617 0.0743773 9.46752C0.141949 8.95727 0.244035 8.45799 0.378206 7.97265C1.64748 3.37143 5.77469 0 10.6705 0C15.5662 0 19.693 3.37143 20.9622 7.97265H15.1526C14.1988 6.47279 12.5479 5.4812 10.6705 5.4812C8.79305 5.4812 7.14216 6.47279 6.18839 7.97265C5.89768 8.42859 5.67212 8.93136 5.52434 9.46752C5.39308 9.94289 5.32308 10.4442 5.32308 10.9624C5.32308 12.5335 5.96768 13.9497 7.00119 14.9487C7.95886 15.876 9.25 16.4436 10.6705 16.4436H24.2577Z" fill="#FD366E"/>
|
||||
<path d="M24.2578 9.46753V14.9487H14.3398C15.3733 13.9497 16.018 12.5335 16.018 10.9624C16.018 10.4442 15.9479 9.9429 15.8167 9.46753H24.2578Z" fill="#FD366E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.1 KiB |
@@ -1,3 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M5 8C5 6.34315 6.34315 5 8 5C9.65685 5 11 6.34315 11 8C11 9.65685 9.65685 11 8 11C6.34315 11 5 9.65685 5 8Z" fill="#D8D8DB"/>
|
||||
<path d="M5 8C5 6.34315 6.34315 5 8 5C9.65685 5 11 6.34315 11 8C11 9.65685 9.65685 11 8 11C6.34315 11 5 9.65685 5 8Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 261 B After Width: | Height: | Size: 266 B |
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
<div class="input-group stack-xs">
|
||||
<label class="label-text typography-text-m-500 text-neutral-secondary">Database</label>
|
||||
<div class="selector-group<?php echo $isDatabaseLocked ? ' is-locked' : ''; ?>">
|
||||
<?php if ($mongoEnabled) { ?>
|
||||
<label class="selector-card <?php echo ($selectedDatabase === 'mongodb') ? 'selected' : ''; ?><?php echo $mongoDisabled ? ' is-disabled has-tooltip' : ''; ?>"<?php echo $mongoDisabled ? ' aria-disabled="true"' : ''; ?>>
|
||||
<input type="radio" name="database" value="mongodb" <?php echo ($selectedDatabase === 'mongodb') ? 'checked' : ''; ?> class="sr-only" <?php echo $mongoDisabled ? 'disabled' : ''; ?>>
|
||||
<div class="selector-content">
|
||||
@@ -54,7 +59,9 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
|
||||
<span class="tooltip tooltip-db-locked typography-text-m-400 text-on-invert" role="tooltip" data-tooltip-portal="true">Database cannot be changed after initial setup.</span>
|
||||
<?php } ?>
|
||||
</label>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($mariaEnabled) { ?>
|
||||
<label class="selector-card <?php echo ($selectedDatabase === 'mariadb') ? 'selected' : ''; ?><?php echo $mariaDisabled ? ' is-disabled has-tooltip' : ''; ?>"<?php echo $mariaDisabled ? ' aria-disabled="true"' : ''; ?>>
|
||||
<input type="radio" name="database" value="mariadb" <?php echo ($selectedDatabase === 'mariadb') ? 'checked' : ''; ?> class="sr-only" <?php echo $mariaDisabled ? 'disabled' : ''; ?>>
|
||||
<div class="selector-content">
|
||||
@@ -66,7 +73,9 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
|
||||
<span class="tooltip tooltip-db-locked typography-text-m-400 text-on-invert" role="tooltip" data-tooltip-portal="true">Database cannot be changed after initial setup.</span>
|
||||
<?php } ?>
|
||||
</label>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($postgresEnabled) { ?>
|
||||
<label class="selector-card <?php echo ($selectedDatabase === 'postgresql') ? 'selected' : ''; ?><?php echo $postgresDisabled ? ' is-disabled has-tooltip' : ''; ?>"<?php echo $postgresDisabled ? ' aria-disabled="true"' : ''; ?>>
|
||||
<input type="radio" name="database" value="postgresql" <?php echo ($selectedDatabase === 'postgresql') ? 'checked' : ''; ?> class="sr-only" <?php echo $postgresDisabled ? 'disabled' : ''; ?>>
|
||||
<div class="selector-content">
|
||||
@@ -78,6 +87,7 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
|
||||
<span class="tooltip tooltip-db-locked typography-text-m-400 text-on-invert" role="tooltip" data-tooltip-portal="true">Database cannot be changed after initial setup.</span>
|
||||
<?php } ?>
|
||||
</label>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -114,6 +114,12 @@
|
||||
"czproject/git-php": "4.*",
|
||||
"laravel/pint": "1.*"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/utopia-php/database"
|
||||
}
|
||||
],
|
||||
"provide": {
|
||||
"ext-phpiredis": "*"
|
||||
},
|
||||
|
||||
Generated
+12
-12
@@ -3850,16 +3850,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/database",
|
||||
"version": "5.3.16",
|
||||
"version": "5.3.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/database.git",
|
||||
"reference": "f121418c4dc7169576735620fd8c17093c3b851d"
|
||||
"reference": "cff2b6ed63d3291b74110d086e16ff089fe05993"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/f121418c4dc7169576735620fd8c17093c3b851d",
|
||||
"reference": "f121418c4dc7169576735620fd8c17093c3b851d",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/cff2b6ed63d3291b74110d086e16ff089fe05993",
|
||||
"reference": "cff2b6ed63d3291b74110d086e16ff089fe05993",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3933,10 +3933,10 @@
|
||||
"utopia"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.16",
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.17",
|
||||
"issues": "https://github.com/utopia-php/database/issues"
|
||||
},
|
||||
"time": "2026-03-19T10:10:02+00:00"
|
||||
"time": "2026-03-20T01:18:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/detector",
|
||||
@@ -5469,16 +5469,16 @@
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "1.11.10",
|
||||
"version": "1.11.11",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "96e6b79a241fc615627a820107ca64bbd1b550a6"
|
||||
"reference": "cfc37c85161a5515af4cd2f9885a811f51a2483a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/96e6b79a241fc615627a820107ca64bbd1b550a6",
|
||||
"reference": "96e6b79a241fc615627a820107ca64bbd1b550a6",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/cfc37c85161a5515af4cd2f9885a811f51a2483a",
|
||||
"reference": "cfc37c85161a5515af4cd2f9885a811f51a2483a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5514,9 +5514,9 @@
|
||||
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/sdk-generator/issues",
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.11.10"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.11.11"
|
||||
},
|
||||
"time": "2026-03-19T05:27:36+00:00"
|
||||
"time": "2026-03-19T16:21:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brianium/paratest",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Enable or disable whether a user can impersonate other users. When impersonation headers are used, the request runs as the target user for API behavior, while internal audit logs still attribute the action to the original impersonator and store the impersonated target details only in internal audit payload data.
|
||||
@@ -924,18 +924,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Tasks/StatsResources.php
|
||||
|
||||
-
|
||||
message: '#^Call to static method error\(\) on an unknown class Utopia\\CLI\\Console\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Tasks/Upgrade.php
|
||||
|
||||
-
|
||||
message: '#^Call to static method log\(\) on an unknown class Utopia\\CLI\\Console\.$#'
|
||||
identifier: class.notFound
|
||||
count: 5
|
||||
path: src/Appwrite/Platform/Tasks/Upgrade.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param references unknown parameter\: \$getProjectDB$#'
|
||||
identifier: parameter.notFound
|
||||
@@ -1200,18 +1188,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Appwrite/Utopia/Database/Documents/User.php
|
||||
|
||||
-
|
||||
message: '#^Call to method isValid\(\) on an unknown class Utopia\\Validator\\Email\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Utopia/Database/Validator/Attributes.php
|
||||
|
||||
-
|
||||
message: '#^Instantiated class Utopia\\Validator\\Email not found\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Utopia/Database/Validator/Attributes.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe call to private method Appwrite\\Utopia\\Request\\Filters\\V17\:\:appendSymbol\(\) through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateMethod
|
||||
|
||||
@@ -274,7 +274,7 @@ class Mapper
|
||||
case \Appwrite\Event\Validator\Event::class:
|
||||
case \Appwrite\Event\Validator\FunctionEvent::class:
|
||||
case \Appwrite\Network\Validator\CNAME::class:
|
||||
case \Appwrite\Network\Validator\Email::class:
|
||||
case \Utopia\Emails\Validator\Email::class:
|
||||
case \Appwrite\Network\Validator\Redirect::class:
|
||||
case \Appwrite\Network\Validator\DNS::class:
|
||||
case \Appwrite\Network\Validator\Origin::class:
|
||||
|
||||
@@ -91,6 +91,7 @@ abstract class Migration
|
||||
'1.7.4' => 'V22',
|
||||
'1.8.0' => 'V23',
|
||||
'1.8.1' => 'V23',
|
||||
'1.9.0' => 'V24',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Migration\Version;
|
||||
|
||||
use Appwrite\Migration\Migration;
|
||||
use Exception;
|
||||
use Throwable;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Conflict;
|
||||
use Utopia\Database\Exception\Structure;
|
||||
use Utopia\Database\Exception\Timeout;
|
||||
|
||||
class V24 extends Migration
|
||||
{
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function execute(): void
|
||||
{
|
||||
/**
|
||||
* Disable SubQueries for Performance.
|
||||
*/
|
||||
$subQueries = [
|
||||
'subQueryAccountKeys',
|
||||
'subQueryAttributes',
|
||||
'subQueryAuthenticators',
|
||||
'subQueryChallenges',
|
||||
'subQueryDevKeys',
|
||||
'subQueryIndexes',
|
||||
'subQueryKeys',
|
||||
'subQueryMemberships',
|
||||
'subQueryOrganizationKeys',
|
||||
'subQueryPlatforms',
|
||||
'subQueryProjectVariables',
|
||||
'subQuerySessions',
|
||||
'subQueryTargets',
|
||||
'subQueryTokens',
|
||||
'subQueryTopicTargets',
|
||||
'subQueryVariables',
|
||||
'subQueryWebhooks',
|
||||
];
|
||||
foreach ($subQueries as $name) {
|
||||
Database::addFilter(
|
||||
$name,
|
||||
fn () => null,
|
||||
fn () => []
|
||||
);
|
||||
}
|
||||
|
||||
Console::info('Migrating collections');
|
||||
$this->migrateCollections();
|
||||
|
||||
if ($this->project->getSequence() != 'console') {
|
||||
Console::info('Migrating Databases');
|
||||
$this->migrateDatabases();
|
||||
}
|
||||
|
||||
Console::info('Migrating Buckets');
|
||||
$this->migrateBuckets();
|
||||
|
||||
Console::info('Migrating documents');
|
||||
$this->forEachDocument($this->migrateDocument(...));
|
||||
|
||||
Console::info('Cleaning up old attributes');
|
||||
$this->cleanupOldAttributes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate Collections.
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception|Throwable
|
||||
*/
|
||||
private function migrateCollections(): void
|
||||
{
|
||||
$projectInternalId = $this->project->getSequence();
|
||||
|
||||
if (empty($projectInternalId)) {
|
||||
throw new Exception('Project ID is null');
|
||||
}
|
||||
|
||||
$collectionType = match ($projectInternalId) {
|
||||
'console' => 'console',
|
||||
default => 'projects',
|
||||
};
|
||||
|
||||
$collections = $this->collections[$collectionType];
|
||||
|
||||
foreach ($collections as $collection) {
|
||||
$id = $collection['$id'];
|
||||
|
||||
if (empty($id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Console::log("Migrating collection \"{$id}\"");
|
||||
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
$this->dbForProject->purgeCachedDocument(Database::METADATA, $id);
|
||||
|
||||
switch ($id) {
|
||||
case 'projects':
|
||||
if ($collectionType === 'console') {
|
||||
$attributes = [
|
||||
'labels',
|
||||
'status',
|
||||
];
|
||||
try {
|
||||
$this->createAttributesFromCollection($this->dbForProject, $id, $attributes);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'keys':
|
||||
if ($collectionType === 'console') {
|
||||
$attributes = [
|
||||
'resourceType',
|
||||
'resourceId',
|
||||
'resourceInternalId',
|
||||
];
|
||||
try {
|
||||
$this->createAttributesFromCollection($this->dbForProject, $id, $attributes);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
try {
|
||||
$this->dbForProject->deleteIndex($id, '_key_project');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to delete index \"_key_project\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
try {
|
||||
$this->createIndexFromCollection($this->dbForProject, $id, '_key_resource');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to create index \"_key_resource\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'rules':
|
||||
if ($collectionType === 'console') {
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->dbForProject, $id, 'logs');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to create attribute \"logs\" in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
$indexesToDelete = [
|
||||
'_key_type',
|
||||
'_key_trigger',
|
||||
'_key_deploymentResourceType',
|
||||
'_key_owner',
|
||||
'_key_region',
|
||||
'_key_piid_riid_rt',
|
||||
];
|
||||
foreach ($indexesToDelete as $index) {
|
||||
try {
|
||||
$this->dbForProject->deleteIndex($id, $index);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to delete index \"$index\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
$indexesToCreate = [
|
||||
'_key_type',
|
||||
'_key_trigger',
|
||||
'_key_deploymentResourceType',
|
||||
'_key_owner',
|
||||
'_key_piid_diid_drt',
|
||||
'_key_region_status_createdAt',
|
||||
];
|
||||
foreach ($indexesToCreate as $index) {
|
||||
try {
|
||||
$this->createIndexFromCollection($this->dbForProject, $id, $index);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to create index \"$index\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'teams':
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->dbForProject, $id, 'labels');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to create attribute \"labels\" in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'databases':
|
||||
if ($collectionType === 'projects') {
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->dbForProject, $id, 'database');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to create attribute \"database\" in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'functions':
|
||||
$attributes = [
|
||||
'deploymentRetention',
|
||||
'startCommand',
|
||||
'buildSpecification',
|
||||
'runtimeSpecification',
|
||||
];
|
||||
try {
|
||||
$this->createAttributesFromCollection($this->dbForProject, $id, $attributes);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'sites':
|
||||
$attributes = [
|
||||
'startCommand',
|
||||
'deploymentRetention',
|
||||
'buildSpecification',
|
||||
'runtimeSpecification',
|
||||
];
|
||||
try {
|
||||
$this->createAttributesFromCollection($this->dbForProject, $id, $attributes);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'deployments':
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->dbForProject, $id, 'startCommand');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to create attribute \"startCommand\" in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'executions':
|
||||
try {
|
||||
$this->dbForProject->deleteIndex($id, '_key_function_internal_id');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to delete index \"_key_function_internal_id\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
$this->createIndexFromCollection($this->dbForProject, $id, '_key_resourceType');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to create index \"_key_resourceType\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'buckets':
|
||||
try {
|
||||
$this->dbForProject->deleteIndex($id, '_fulltext_name');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to delete index \"_fulltext_name\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'providers':
|
||||
try {
|
||||
$this->dbForProject->deleteIndex($id, '_key_name');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to delete index \"_key_name\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'topics':
|
||||
try {
|
||||
$this->dbForProject->deleteIndex($id, '_key_name');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to delete index \"_key_name\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate all Database tables
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
private function migrateDatabases(): void
|
||||
{
|
||||
$this->dbForProject->foreach('databases', function (Document $database) {
|
||||
Console::log("Migrating Collections of {$database->getId()} ({$database->getAttribute('name')})");
|
||||
|
||||
$databaseTable = "database_{$database->getSequence()}";
|
||||
$this->dbForProject->purgeCachedCollection($databaseTable);
|
||||
|
||||
$this->dbForProject->foreach($databaseTable, function (Document $collection) use ($databaseTable) {
|
||||
Console::log("Migrating Collection of {$collection->getId()} ({$collection->getAttribute('name')})");
|
||||
|
||||
$collectionTable = "{$databaseTable}_collection_{$collection->getSequence()}";
|
||||
$this->dbForProject->purgeCachedCollection($collectionTable);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate all Bucket tables
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
* @throws \PDOException
|
||||
*/
|
||||
protected function migrateBuckets(): void
|
||||
{
|
||||
$this->dbForProject->foreach('buckets', function (Document $bucket) {
|
||||
Console::log("Migrating Bucket {$bucket->getId()} ({$bucket->getAttribute('name')})");
|
||||
|
||||
$bucketTable = "bucket_{$bucket->getSequence()}";
|
||||
$this->dbForProject->purgeCachedCollection($bucketTable);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix run on each document
|
||||
*
|
||||
* @param Document $document
|
||||
* @return Document
|
||||
* @throws Conflict
|
||||
* @throws Structure
|
||||
* @throws Timeout
|
||||
* @throws \Utopia\Database\Exception
|
||||
* @throws \Utopia\Database\Exception\Authorization
|
||||
* @throws \Utopia\Database\Exception\Query
|
||||
*/
|
||||
private function migrateDocument(Document $document): Document
|
||||
{
|
||||
switch ($document->getCollection()) {
|
||||
case 'keys':
|
||||
$projectInternalId = $document->getAttribute('projectInternalId');
|
||||
$projectId = $document->getAttribute('projectId');
|
||||
|
||||
if (!empty($projectInternalId) && empty($document->getAttribute('resourceInternalId'))) {
|
||||
$document->setAttribute('resourceType', 'projects');
|
||||
$document->setAttribute('resourceId', $projectId);
|
||||
$document->setAttribute('resourceInternalId', $projectInternalId);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return $document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old attributes after document migration is complete.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function cleanupOldAttributes(): void
|
||||
{
|
||||
$collectionType = match ($this->project->getSequence()) {
|
||||
'console' => 'console',
|
||||
default => 'projects',
|
||||
};
|
||||
|
||||
if ($collectionType === 'console') {
|
||||
$attributesToDelete = [
|
||||
'keys' => ['projectInternalId', 'projectId'],
|
||||
];
|
||||
|
||||
foreach ($attributesToDelete as $collectionId => $attributes) {
|
||||
foreach ($attributes as $attribute) {
|
||||
try {
|
||||
$this->dbForProject->deleteAttribute($collectionId, $attribute);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to delete attribute \"{$attribute}\" from {$collectionId}: {$th->getMessage()}");
|
||||
}
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($collectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ class Platform
|
||||
public const SCHEME_ANDROID = 'appwrite-android';
|
||||
public const SCHEME_WINDOWS = 'appwrite-windows';
|
||||
public const SCHEME_LINUX = 'appwrite-linux';
|
||||
public const SCHEME_TAURI = 'tauri';
|
||||
|
||||
/**
|
||||
* @var array<string, string> Map scheme types to user-friendly platform names.
|
||||
@@ -53,6 +54,7 @@ class Platform
|
||||
self::SCHEME_FIREFOX_EXTENSION => 'Web (Firefox Extension)',
|
||||
self::SCHEME_SAFARI_EXTENSION => 'Web (Safari Extension)',
|
||||
self::SCHEME_EDGE_EXTENSION => 'Web (Edge Extension)',
|
||||
self::SCHEME_TAURI => 'Web (Tauri)',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Network\Validator;
|
||||
|
||||
use Utopia\Validator;
|
||||
|
||||
/**
|
||||
* Email
|
||||
*
|
||||
* Validate that an variable is a valid email address
|
||||
*
|
||||
* @package Utopia\Validator
|
||||
*/
|
||||
class Email extends Validator
|
||||
{
|
||||
protected bool $allowEmpty;
|
||||
|
||||
public function __construct(bool $allowEmpty = false)
|
||||
{
|
||||
$this->allowEmpty = $allowEmpty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Description
|
||||
*
|
||||
* Returns validator description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Value must be a valid email address';
|
||||
}
|
||||
|
||||
/**
|
||||
* Is valid
|
||||
*
|
||||
* Validation will pass when $value is valid email address.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid($value): bool
|
||||
{
|
||||
if ($this->allowEmpty && \strlen($value) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!\filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is array
|
||||
*
|
||||
* Function will return true if object is array.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isArray(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* Returns validator type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return self::TYPE_STRING;
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ class Origin extends Validator
|
||||
Platform::SCHEME_FIREFOX_EXTENSION,
|
||||
Platform::SCHEME_SAFARI_EXTENSION,
|
||||
Platform::SCHEME_EDGE_EXTENSION,
|
||||
Platform::SCHEME_TAURI,
|
||||
];
|
||||
if (in_array($this->scheme, $webPlatforms, true)) {
|
||||
$validator = new Hostname($this->allowedHostnames);
|
||||
|
||||
@@ -48,6 +48,16 @@ class Complete extends Action
|
||||
|
||||
@touch(Server::INSTALLER_COMPLETE_FILE);
|
||||
|
||||
if (!$sessionSecret && $installId !== '') {
|
||||
$data = $state->readProgressFile($installId);
|
||||
$details = $data['details'][Server::STEP_ACCOUNT_SETUP] ?? [];
|
||||
if (!empty($details['sessionSecret'])) {
|
||||
$sessionSecret = $details['sessionSecret'];
|
||||
$sessionId = $sessionId ?: ($details['sessionId'] ?? '');
|
||||
$sessionExpire = $sessionExpire ?: ($details['sessionExpire'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
if ($sessionSecret) {
|
||||
$isHttps = $request->getProtocol() === 'https';
|
||||
$sameSite = $isHttps ? Response::COOKIE_SAMESITE_NONE : Response::COOKIE_SAMESITE_LAX;
|
||||
|
||||
@@ -127,6 +127,10 @@ class Install extends Action
|
||||
$this->sendBadRequest($response, $swooleResponse, $wantsStream, 'Please select a supported database');
|
||||
return;
|
||||
}
|
||||
if (!$config->isDatabaseEnabled($database)) {
|
||||
$this->sendBadRequest($response, $swooleResponse, $wantsStream, 'The selected database is not available');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$installId = $state->sanitizeInstallId($installId);
|
||||
|
||||
@@ -44,6 +44,7 @@ class View extends Action
|
||||
$defaultHttpsPort = $config->getDefaultHttpsPort();
|
||||
$isUpgrade = $config->isUpgrade();
|
||||
$lockedDatabase = $config->getLockedDatabase();
|
||||
$enabledDatabases = $config->getEnabledDatabases();
|
||||
$isLocalInstall = $config->isLocal();
|
||||
|
||||
$defaultEmailCertificates = $vars['_APP_EMAIL_CERTIFICATES']['default'] ?? '';
|
||||
|
||||
@@ -14,6 +14,7 @@ final class Config
|
||||
'isLocal',
|
||||
'hostPath',
|
||||
'lockedDatabase',
|
||||
'enabledDatabases',
|
||||
'vars',
|
||||
];
|
||||
|
||||
@@ -26,6 +27,7 @@ final class Config
|
||||
private bool $isLocal = false;
|
||||
private ?string $hostPath = null;
|
||||
private ?string $lockedDatabase = null;
|
||||
private array $enabledDatabases = ['mongodb', 'mariadb'];
|
||||
private array $vars = [];
|
||||
|
||||
public function __construct(array $values = [])
|
||||
@@ -67,6 +69,9 @@ final class Config
|
||||
if ($this->hasValidStringValue($values, 'lockedDatabase')) {
|
||||
$this->setLockedDatabase((string) $values['lockedDatabase']);
|
||||
}
|
||||
if (array_key_exists('enabledDatabases', $values) && is_array($values['enabledDatabases'])) {
|
||||
$this->setEnabledDatabases($values['enabledDatabases']);
|
||||
}
|
||||
if (array_key_exists('vars', $values) && is_array($values['vars'])) {
|
||||
$this->setVars($values['vars']);
|
||||
}
|
||||
@@ -100,6 +105,7 @@ final class Config
|
||||
'isLocal' => $this->isLocal,
|
||||
'hostPath' => $this->hostPath,
|
||||
'lockedDatabase' => $this->lockedDatabase,
|
||||
'enabledDatabases' => $this->enabledDatabases,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -202,4 +208,28 @@ final class Config
|
||||
{
|
||||
$this->lockedDatabase = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getEnabledDatabases(): array
|
||||
{
|
||||
return $this->enabledDatabases;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $value
|
||||
*/
|
||||
public function setEnabledDatabases(array $value): void
|
||||
{
|
||||
$filtered = array_values(array_filter($value, fn ($v) => is_string($v) && $v !== ''));
|
||||
if (!empty($filtered)) {
|
||||
$this->enabledDatabases = $filtered;
|
||||
}
|
||||
}
|
||||
|
||||
public function isDatabaseEnabled(string $adapter): bool
|
||||
{
|
||||
return in_array($adapter, $this->enabledDatabases, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +207,53 @@ class State
|
||||
return sys_get_temp_dir() . '/appwrite-install-' . $installId . '.json';
|
||||
}
|
||||
|
||||
public function clearStaleLock(): void
|
||||
{
|
||||
$this->withGlobalLock(function ($handle, $lock) {
|
||||
if (!$handle) {
|
||||
return;
|
||||
}
|
||||
if ($lock === null) {
|
||||
return;
|
||||
}
|
||||
if ($this->isGlobalLockActive($lock)) {
|
||||
return;
|
||||
}
|
||||
ftruncate($handle, 0);
|
||||
rewind($handle);
|
||||
});
|
||||
|
||||
$tempDir = sys_get_temp_dir();
|
||||
foreach ((array) glob($tempDir . '/appwrite-install-*.json') as $file) {
|
||||
$contents = @file_get_contents($file);
|
||||
if ($contents === false) {
|
||||
continue;
|
||||
}
|
||||
$data = json_decode($contents, true);
|
||||
if (!is_array($data)) {
|
||||
@unlink($file);
|
||||
continue;
|
||||
}
|
||||
$updatedAt = $data['updatedAt'] ?? 0;
|
||||
$age = time() - (int) $updatedAt;
|
||||
$isTerminal = isset($data['error']);
|
||||
if (!$isTerminal && !empty($data['steps'])) {
|
||||
$isTerminal = true;
|
||||
foreach ($data['steps'] as $step) {
|
||||
if (($step['status'] ?? '') !== Server::STATUS_COMPLETED) {
|
||||
$isTerminal = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($age > self::GLOBAL_LOCK_TIMEOUT_SECONDS) {
|
||||
@unlink($file);
|
||||
} elseif ($isTerminal && $age > 60) {
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function reserveGlobalLock(string $installId): string
|
||||
{
|
||||
return (string) $this->withGlobalLock(function ($handle, $lock) use ($installId) {
|
||||
|
||||
@@ -127,6 +127,8 @@ class Server
|
||||
|
||||
private function startSwooleServer(string $host, int $port, ?string $readyFile = null): void
|
||||
{
|
||||
$this->state->clearStaleLock();
|
||||
|
||||
// Preload static files into memory
|
||||
$files = new Files();
|
||||
$files->load($this->paths['views']);
|
||||
|
||||
+1
-1
@@ -4,7 +4,6 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attribu
|
||||
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
@@ -16,6 +15,7 @@ use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Key;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Nullable;
|
||||
|
||||
+1
-1
@@ -3,7 +3,6 @@
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Email;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
@@ -15,6 +14,7 @@ use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Key;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Nullable;
|
||||
|
||||
+1
-1
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Email;
|
||||
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Email\Create as EmailCreate;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
@@ -11,6 +10,7 @@ use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Key;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Nullable;
|
||||
|
||||
+1
-1
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Email;
|
||||
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Email\Update as EmailUpdate;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
@@ -12,6 +11,7 @@ use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Key;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Nullable;
|
||||
|
||||
@@ -7,7 +7,6 @@ use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Validator\Email as EmailValidator;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
@@ -32,6 +31,7 @@ use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Key;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Emails\Email;
|
||||
use Utopia\Emails\Validator\Email as EmailValidator;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\System\System;
|
||||
|
||||
@@ -39,6 +39,7 @@ class Create extends Base
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/webhooks')
|
||||
->httpAlias('/v1/projects/:projectId/webhooks')
|
||||
->desc('Create webhook')
|
||||
->groups(['api', 'webhooks'])
|
||||
->label('scope', 'webhooks.write')
|
||||
|
||||
@@ -32,6 +32,7 @@ class Delete extends Base
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
|
||||
->setHttpPath('/v1/webhooks/:webhookId')
|
||||
->httpAlias('/v1/projects/:projectId/webhooks/:webhookId')
|
||||
->desc('Delete webhook')
|
||||
->groups(['api', 'webhooks'])
|
||||
->label('scope', 'webhooks.write')
|
||||
|
||||
@@ -30,6 +30,7 @@ class Get extends Base
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/webhooks/:webhookId')
|
||||
->httpAlias('/v1/projects/:projectId/webhooks/:webhookId')
|
||||
->desc('Get webhook')
|
||||
->groups(['api', 'webhooks'])
|
||||
->label('scope', 'webhooks.read')
|
||||
|
||||
@@ -30,6 +30,7 @@ class Update extends Base
|
||||
{
|
||||
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/webhooks/:webhookId/signature')
|
||||
->httpAlias('/v1/projects/:projectId/webhooks/:webhookId/signature')
|
||||
->desc('Update webhook signature key')
|
||||
->groups(['api', 'webhooks'])
|
||||
->label('scope', 'webhooks.write')
|
||||
|
||||
@@ -37,6 +37,7 @@ class Update extends Base
|
||||
{
|
||||
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
|
||||
->setHttpPath('/v1/webhooks/:webhookId')
|
||||
->httpAlias('/v1/projects/:projectId/webhooks/:webhookId')
|
||||
->desc('Update webhook')
|
||||
->groups(['api', 'webhooks'])
|
||||
->label('scope', 'webhooks.write')
|
||||
|
||||
@@ -34,6 +34,7 @@ class XList extends Base
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/webhooks')
|
||||
->httpAlias('/v1/projects/:projectId/webhooks')
|
||||
->desc('List webhooks')
|
||||
->groups(['api', 'webhooks'])
|
||||
->label('scope', 'webhooks.read')
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Tasks;
|
||||
|
||||
use Appwrite\Docker\Compose;
|
||||
use Appwrite\Docker\Env;
|
||||
use Appwrite\Platform\Installer\Runtime\State;
|
||||
use Appwrite\Platform\Installer\Server as InstallerServer;
|
||||
use Appwrite\Utopia\View;
|
||||
use Utopia\Auth\Proofs\Password;
|
||||
@@ -23,7 +24,7 @@ class Install extends Action
|
||||
private const int WEB_SERVER_CHECK_DELAY_SECONDS = 1;
|
||||
|
||||
private const int HEALTH_CHECK_ATTEMPTS = 30;
|
||||
private const int HEALTH_CHECK_DELAY_SECONDS = 3;
|
||||
private const int HEALTH_CHECK_DELAY_SECONDS = 1;
|
||||
|
||||
private const string PATTERN_ENV_VAR_NAME = '/^[A-Z0-9_]+$/';
|
||||
private const string PATTERN_DB_PASSWORD_VAR = '/^_APP_DB_.*_PASS$/';
|
||||
@@ -167,8 +168,27 @@ class Install extends Action
|
||||
}
|
||||
}
|
||||
|
||||
// Block database type changes on existing installations
|
||||
$existingDatabase = $vars['_APP_DB_ADAPTER']['default'] ?? null;
|
||||
// Block database type changes on existing installations.
|
||||
// Only enforce if the existing config explicitly set _APP_DB_ADAPTER
|
||||
// (pre-1.9.0 installs never had this variable).
|
||||
$existingDatabase = null;
|
||||
foreach ($compose->getServices() as $service) {
|
||||
if (!$service) {
|
||||
continue;
|
||||
}
|
||||
$svcEnv = $service->getEnvironment()->list();
|
||||
if (isset($svcEnv['_APP_DB_ADAPTER'])) {
|
||||
$existingDatabase = $svcEnv['_APP_DB_ADAPTER'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($existingDatabase === null) {
|
||||
$envFilePath = $this->path . '/' . $this->getEnvFileName();
|
||||
$rawEnv = @file_get_contents($envFilePath);
|
||||
if ($rawEnv !== false) {
|
||||
$existingDatabase = (new Env($rawEnv))->list()['_APP_DB_ADAPTER'] ?? null;
|
||||
}
|
||||
}
|
||||
if ($existingDatabase !== null && $existingDatabase !== $database) {
|
||||
Console::error("Cannot change database type from '{$existingDatabase}' to '{$database}'.");
|
||||
Console::error('Changing database types on an existing installation is not supported.');
|
||||
@@ -176,6 +196,13 @@ class Install extends Action
|
||||
}
|
||||
}
|
||||
|
||||
$installerConfig = $this->readInstallerConfig();
|
||||
$enabledDatabases = $installerConfig['enabledDatabases'] ?? ['mongodb', 'mariadb'];
|
||||
if (!in_array($database, $enabledDatabases, true)) {
|
||||
Console::error("Database '{$database}' is not available. Available options: " . implode(', ', $enabledDatabases));
|
||||
Console::exit(1);
|
||||
}
|
||||
|
||||
// If interactive and web mode enabled, start web server
|
||||
if ($interactive === 'Y' && Console::isInteractive()) {
|
||||
Console::success('Starting web installer...');
|
||||
@@ -271,7 +298,8 @@ class Install extends Action
|
||||
Console::warning("\nUse 'AAAA' if you're using an IPv6 address and 'A' if you're using an IPv4 address.\n");
|
||||
}
|
||||
}
|
||||
$database = $userInput['_APP_DB_ADAPTER'] ?? $database;
|
||||
$userInput['_APP_DB_ADAPTER'] = $userInput['_APP_DB_ADAPTER'] ?? $database;
|
||||
$database = $userInput['_APP_DB_ADAPTER'];
|
||||
if ($database === 'postgresql') {
|
||||
$userInput['_APP_DB_HOST'] = 'postgresql';
|
||||
$userInput['_APP_DB_PORT'] = 5432;
|
||||
@@ -295,6 +323,12 @@ class Install extends Action
|
||||
|
||||
@unlink(InstallerServer::INSTALLER_COMPLETE_FILE);
|
||||
|
||||
$state = new State([]);
|
||||
$state->clearStaleLock();
|
||||
|
||||
$installerConfig = $this->readInstallerConfig();
|
||||
$enabledDatabases = $installerConfig['enabledDatabases'] ?? ['mongodb', 'mariadb'];
|
||||
|
||||
$this->setInstallerConfig([
|
||||
'defaultHttpPort' => $defaultHttpPort,
|
||||
'defaultHttpsPort' => $defaultHttpsPort,
|
||||
@@ -304,6 +338,7 @@ class Install extends Action
|
||||
'vars' => $vars,
|
||||
'isUpgrade' => $isUpgrade,
|
||||
'lockedDatabase' => $lockedDatabase,
|
||||
'enabledDatabases' => $enabledDatabases,
|
||||
'isLocal' => $this->isLocalInstall(),
|
||||
'hostPath' => $this->hostPath ?: null,
|
||||
]);
|
||||
@@ -734,8 +769,8 @@ class Install extends Action
|
||||
{
|
||||
$client = new Client();
|
||||
$client
|
||||
->setTimeout(5000)
|
||||
->setConnectTimeout(5000)
|
||||
->setTimeout(2000)
|
||||
->setConnectTimeout(2000)
|
||||
->addHeader('Host', $domain);
|
||||
|
||||
$healthPath = '/v1/health/version';
|
||||
|
||||
@@ -31,7 +31,7 @@ class Migrate extends Action
|
||||
->inject('dbForPlatform')
|
||||
->inject('getProjectDB')
|
||||
->inject('register')
|
||||
->inject('authorisation')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -64,8 +64,43 @@ class Migrate extends Action
|
||||
/** @var Migration $migration */
|
||||
$migration = new $class();
|
||||
|
||||
// Disable subquery filters that reference new schema columns not yet migrated
|
||||
$subQueries = [
|
||||
'subQueryAccountKeys',
|
||||
'subQueryAttributes',
|
||||
'subQueryAuthenticators',
|
||||
'subQueryChallenges',
|
||||
'subQueryDevKeys',
|
||||
'subQueryIndexes',
|
||||
'subQueryKeys',
|
||||
'subQueryMemberships',
|
||||
'subQueryOrganizationKeys',
|
||||
'subQueryPlatforms',
|
||||
'subQueryProjectVariables',
|
||||
'subQuerySessions',
|
||||
'subQueryTargets',
|
||||
'subQueryTokens',
|
||||
'subQueryTopicTargets',
|
||||
'subQueryVariables',
|
||||
'subQueryWebhooks',
|
||||
];
|
||||
foreach ($subQueries as $name) {
|
||||
Database::addFilter(
|
||||
$name,
|
||||
fn () => null,
|
||||
fn () => []
|
||||
);
|
||||
}
|
||||
|
||||
$dbForPlatform->disableValidation();
|
||||
$dbForPlatform->purgeCachedCollection('projects');
|
||||
|
||||
$count = 0;
|
||||
$total = $dbForPlatform->count('projects') + 1;
|
||||
try {
|
||||
$total = $dbForPlatform->count('projects') + 1;
|
||||
} catch (\Throwable) {
|
||||
$total = 0;
|
||||
}
|
||||
|
||||
$dbForPlatform->foreach('projects', function (Document $project) use ($dbForPlatform, $getProjectDB, $register, $migration, &$count, $total, $authorization) {
|
||||
/** @var Database $dbForProject */
|
||||
@@ -74,9 +109,14 @@ class Migrate extends Action
|
||||
|
||||
try {
|
||||
$migration
|
||||
->setProject($project, $dbForProject, $dbForPlatform, $authorization, $getProjectDB)
|
||||
->setPDO($register->get('db', true))
|
||||
->execute();
|
||||
->setProject($project, $dbForProject, $dbForPlatform, $authorization, $getProjectDB);
|
||||
|
||||
$db = $register->get('db', true);
|
||||
if ($db instanceof \Utopia\Database\PDO) {
|
||||
$migration->setPDO($db);
|
||||
}
|
||||
|
||||
$migration->execute();
|
||||
} catch (\Throwable $th) {
|
||||
Console::error('Failed to migrate project "' . $project->getId() . '" with error: ' . $th->getMessage());
|
||||
throw $th;
|
||||
@@ -89,9 +129,14 @@ class Migrate extends Action
|
||||
|
||||
try {
|
||||
$migration
|
||||
->setProject($console, $getProjectDB($console), $dbForPlatform, $authorization, $getProjectDB)
|
||||
->setPDO($register->get('db', true))
|
||||
->execute();
|
||||
->setProject($console, $getProjectDB($console), $dbForPlatform, $authorization, $getProjectDB);
|
||||
|
||||
$db = $register->get('db', true);
|
||||
if ($db instanceof \Utopia\Database\PDO) {
|
||||
$migration->setPDO($db);
|
||||
}
|
||||
|
||||
$migration->execute();
|
||||
} catch (\Throwable $th) {
|
||||
Console::error('Failed to migrate project "console" with error: ' . $th->getMessage());
|
||||
throw $th;
|
||||
|
||||
@@ -159,7 +159,25 @@ class Specs extends Action
|
||||
'name' => 'X-Appwrite-Dev-Key',
|
||||
'description' => 'Your secret dev API key',
|
||||
'in' => 'header',
|
||||
]
|
||||
],
|
||||
'ImpersonateUserId' => [
|
||||
'type' => 'apiKey',
|
||||
'name' => 'X-Appwrite-Impersonate-User-Id',
|
||||
'description' => 'Impersonate a user by ID on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.',
|
||||
'in' => 'header',
|
||||
],
|
||||
'ImpersonateUserEmail' => [
|
||||
'type' => 'apiKey',
|
||||
'name' => 'X-Appwrite-Impersonate-User-Email',
|
||||
'description' => 'Impersonate a user by email on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.',
|
||||
'in' => 'header',
|
||||
],
|
||||
'ImpersonateUserPhone' => [
|
||||
'type' => 'apiKey',
|
||||
'name' => 'X-Appwrite-Impersonate-User-Phone',
|
||||
'description' => 'Impersonate a user by phone on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.',
|
||||
'in' => 'header',
|
||||
],
|
||||
],
|
||||
APP_SDK_PLATFORM_SERVER => [
|
||||
'Project' => [
|
||||
@@ -198,6 +216,24 @@ class Specs extends Action
|
||||
'description' => 'The user agent string of the client that made the request',
|
||||
'in' => 'header',
|
||||
],
|
||||
'ImpersonateUserId' => [
|
||||
'type' => 'apiKey',
|
||||
'name' => 'X-Appwrite-Impersonate-User-Id',
|
||||
'description' => 'Impersonate a user by ID on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.',
|
||||
'in' => 'header',
|
||||
],
|
||||
'ImpersonateUserEmail' => [
|
||||
'type' => 'apiKey',
|
||||
'name' => 'X-Appwrite-Impersonate-User-Email',
|
||||
'description' => 'Impersonate a user by email on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.',
|
||||
'in' => 'header',
|
||||
],
|
||||
'ImpersonateUserPhone' => [
|
||||
'type' => 'apiKey',
|
||||
'name' => 'X-Appwrite-Impersonate-User-Phone',
|
||||
'description' => 'Impersonate a user by phone on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.',
|
||||
'in' => 'header',
|
||||
],
|
||||
],
|
||||
APP_SDK_PLATFORM_CONSOLE => [
|
||||
'Project' => [
|
||||
@@ -236,6 +272,24 @@ class Specs extends Action
|
||||
'description' => 'The user cookie to authenticate with',
|
||||
'in' => 'header',
|
||||
],
|
||||
'ImpersonateUserId' => [
|
||||
'type' => 'apiKey',
|
||||
'name' => 'X-Appwrite-Impersonate-User-Id',
|
||||
'description' => 'Impersonate a user by ID on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.',
|
||||
'in' => 'header',
|
||||
],
|
||||
'ImpersonateUserEmail' => [
|
||||
'type' => 'apiKey',
|
||||
'name' => 'X-Appwrite-Impersonate-User-Email',
|
||||
'description' => 'Impersonate a user by email on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.',
|
||||
'in' => 'header',
|
||||
],
|
||||
'ImpersonateUserPhone' => [
|
||||
'type' => 'apiKey',
|
||||
'name' => 'X-Appwrite-Impersonate-User-Phone',
|
||||
'description' => 'Impersonate a user by phone on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.',
|
||||
'in' => 'header',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace Appwrite\Platform\Tasks;
|
||||
|
||||
use Appwrite\Docker\Compose;
|
||||
use Appwrite\Docker\Env;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Console;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
@@ -80,7 +80,9 @@ class Upgrade extends Install
|
||||
}
|
||||
|
||||
if ($database === null) {
|
||||
throw new \Exception('Database type not found, can not upgrade. Ensure `_APP_DB_ADAPTER` is set in your environment.');
|
||||
// Pre-1.9.0 installations only supported MariaDB
|
||||
$database = 'mariadb';
|
||||
Console::info('No _APP_DB_ADAPTER found in existing configuration, defaulting to mariadb.');
|
||||
}
|
||||
|
||||
$this->lockedDatabase = $database;
|
||||
|
||||
@@ -82,22 +82,31 @@ class Audits extends Action
|
||||
$ip = $payload['ip'] ?? '';
|
||||
$user = new Document($payload['user'] ?? []);
|
||||
|
||||
$userName = $user->getAttribute('name', '');
|
||||
$userEmail = $user->getAttribute('email', '');
|
||||
$impersonatorUserId = $user->getAttribute('impersonatorUserId');
|
||||
$actorUserId = $impersonatorUserId ?: $user->getId();
|
||||
$actorUserInternalId = $impersonatorUserId
|
||||
? $user->getAttribute('impersonatorUserInternalId')
|
||||
: $user->getSequence();
|
||||
$actorUserName = $impersonatorUserId
|
||||
? $user->getAttribute('impersonatorUserName', '')
|
||||
: $user->getAttribute('name', '');
|
||||
$actorUserEmail = $impersonatorUserId
|
||||
? $user->getAttribute('impersonatorUserEmail', '')
|
||||
: $user->getAttribute('email', '');
|
||||
$userType = $user->getAttribute('type', ACTIVITY_TYPE_USER);
|
||||
|
||||
// Create event data
|
||||
$eventData = [
|
||||
'userId' => $user->getSequence(),
|
||||
'userId' => $actorUserInternalId,
|
||||
'event' => $event,
|
||||
'resource' => $resource,
|
||||
'userAgent' => $userAgent,
|
||||
'ip' => $ip,
|
||||
'location' => '',
|
||||
'data' => [
|
||||
'userId' => $user->getId(),
|
||||
'userName' => $userName,
|
||||
'userEmail' => $userEmail,
|
||||
'userId' => $actorUserId,
|
||||
'userName' => $actorUserName,
|
||||
'userEmail' => $actorUserEmail,
|
||||
'userType' => $userType,
|
||||
'mode' => $mode,
|
||||
'data' => $auditPayload,
|
||||
@@ -105,6 +114,21 @@ class Audits extends Action
|
||||
'time' => date("Y-m-d H:i:s", $message->getTimestamp()),
|
||||
];
|
||||
|
||||
if (!empty($impersonatorUserId)) {
|
||||
$eventData['data']['data'] = \is_array($auditPayload)
|
||||
? \array_merge($auditPayload, [
|
||||
'impersonatedUserId' => $user->getId(),
|
||||
'impersonatedUserName' => $user->getAttribute('name', ''),
|
||||
'impersonatedUserEmail' => $user->getAttribute('email', ''),
|
||||
])
|
||||
: [
|
||||
'payload' => $auditPayload,
|
||||
'impersonatedUserId' => $user->getId(),
|
||||
'impersonatedUserName' => $user->getAttribute('name', ''),
|
||||
'impersonatedUserEmail' => $user->getAttribute('email', ''),
|
||||
];
|
||||
}
|
||||
|
||||
if (isset($this->logs[$project->getSequence()])) {
|
||||
$this->logs[$project->getSequence()]['logs'][] = $eventData;
|
||||
} else {
|
||||
|
||||
@@ -450,7 +450,7 @@ class OpenAPI3 extends Format
|
||||
Database::VAR_POLYGON => '[[[1, 2], [3, 4], [5, 6], [1, 2]]]',
|
||||
};
|
||||
break;
|
||||
case \Appwrite\Network\Validator\Email::class:
|
||||
case \Utopia\Emails\Validator\Email::class:
|
||||
$node['schema']['type'] = $validator->getType();
|
||||
$node['schema']['format'] = 'email';
|
||||
$node['schema']['x-example'] = ($param['example'] ?? '') ?: 'email@example.com';
|
||||
@@ -843,8 +843,7 @@ class OpenAPI3 extends Format
|
||||
break;
|
||||
|
||||
case 'id':
|
||||
$type = 'integer';
|
||||
$format = $rule['format'] ?? 'int32';
|
||||
$type = 'string';
|
||||
break;
|
||||
|
||||
case 'enum':
|
||||
|
||||
@@ -455,7 +455,7 @@ class Swagger2 extends Format
|
||||
Database::VAR_POLYGON => '[[[1, 2], [3, 4], [5, 6], [1, 2]]]',
|
||||
};
|
||||
break;
|
||||
case \Appwrite\Network\Validator\Email::class:
|
||||
case \Utopia\Emails\Validator\Email::class:
|
||||
$node['type'] = $validator->getType();
|
||||
$node['format'] = 'email';
|
||||
$node['x-example'] = ($param['example'] ?? '') ?: 'email@example.com';
|
||||
@@ -823,8 +823,7 @@ class Swagger2 extends Format
|
||||
break;
|
||||
|
||||
case 'id':
|
||||
$type = 'integer';
|
||||
$format = $rule['format'] ?? 'int32';
|
||||
$type = 'string';
|
||||
break;
|
||||
|
||||
case 'enum':
|
||||
|
||||
@@ -5,8 +5,8 @@ namespace Appwrite\Utopia\Database\Validator;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Database\Validator\Key;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Validator;
|
||||
use Utopia\Validator\Email;
|
||||
use Utopia\Validator\IP;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
@@ -14,6 +14,7 @@ class Users extends Base
|
||||
'emailVerification',
|
||||
'phoneVerification',
|
||||
'labels',
|
||||
'impersonator',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,9 @@ class V21 extends Filter
|
||||
public function parse(array $content, string $model): array
|
||||
{
|
||||
switch ($model) {
|
||||
case 'webhooks.create':
|
||||
$content = $this->fillWebhookid($content);
|
||||
break;
|
||||
case 'functions.createTemplateDeployment':
|
||||
case 'sites.createTemplateDeployment':
|
||||
$content = $this->convertVersionToTypeAndReference($content);
|
||||
@@ -48,4 +51,10 @@ class V21 extends Filter
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function fillWebhookid(array $content): array
|
||||
{
|
||||
$content['webhookId'] = $content['webhookId'] ?? 'unique()';
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,18 @@ class V21 extends Filter
|
||||
"functions",
|
||||
fn ($item) => $this->parseFunction($item),
|
||||
),
|
||||
Response::MODEL_DOCUMENT => $this->parseDocument($content),
|
||||
Response::MODEL_DOCUMENT_LIST => $this->handleList(
|
||||
$content,
|
||||
"documents",
|
||||
fn ($item) => $this->parseDocument($item),
|
||||
),
|
||||
Response::MODEL_ROW => $this->parseRow($content),
|
||||
Response::MODEL_ROW_LIST => $this->handleList(
|
||||
$content,
|
||||
"rows",
|
||||
fn ($item) => $this->parseRow($item),
|
||||
),
|
||||
default => $content,
|
||||
};
|
||||
}
|
||||
@@ -46,4 +58,39 @@ class V21 extends Filter
|
||||
unset($content['runtimeSpecification']);
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function parseDocument(array $content): array
|
||||
{
|
||||
return $this->castSequence($content);
|
||||
}
|
||||
|
||||
protected function parseRow(array $content): array
|
||||
{
|
||||
return $this->castSequence($content);
|
||||
}
|
||||
|
||||
protected function castSequence(array $content): array
|
||||
{
|
||||
if (isset($content['$sequence'])) {
|
||||
$content['$sequence'] = \is_numeric($content['$sequence'])
|
||||
? (int)$content['$sequence']
|
||||
: 0;
|
||||
}
|
||||
|
||||
foreach ($content as $key => $value) {
|
||||
if (\is_array($value)) {
|
||||
if (isset($value['$id'])) {
|
||||
$content[$key] = $this->castSequence($value);
|
||||
} else {
|
||||
foreach ($value as $i => $item) {
|
||||
if (\is_array($item) && isset($item['$id'])) {
|
||||
$content[$key][$i] = $this->castSequence($item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,20 @@ class Account extends Model
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('impersonator', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Whether the user can impersonate other users.',
|
||||
'required' => false,
|
||||
'default' => false,
|
||||
'example' => false,
|
||||
])
|
||||
->addRule('impersonatorUserId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'ID of the original actor performing the impersonation. Present only when the current request is impersonating another user. Internal audit logs attribute the action to this user, while the impersonated target is recorded only in internal audit payload data.',
|
||||
'required' => false,
|
||||
'default' => '',
|
||||
'example' => '5e5ea5c16897e',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -83,8 +83,8 @@ class Document extends Any
|
||||
$document->removeAttribute('$collection');
|
||||
$document->removeAttribute('$tenant');
|
||||
|
||||
if (!$document->isEmpty() && \is_numeric($document->getAttribute('$sequence', 0))) {
|
||||
$document->setAttribute('$sequence', (int)$document->getAttribute('$sequence', 0));
|
||||
if (!$document->isEmpty()) {
|
||||
$document->setAttribute('$sequence', (string)$document->getAttribute('$sequence', ''));
|
||||
}
|
||||
|
||||
foreach ($document->getAttributes() as $attribute) {
|
||||
|
||||
@@ -18,19 +18,19 @@ class Log extends Model
|
||||
])
|
||||
->addRule('userId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'User ID.',
|
||||
'description' => 'User ID of the actor recorded for this log. During impersonation, this is the original impersonator, not the impersonated target user.',
|
||||
'default' => '',
|
||||
'example' => '610fc2f985ee0',
|
||||
])
|
||||
->addRule('userEmail', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'User Email.',
|
||||
'description' => 'User email of the actor recorded for this log. During impersonation, this is the original impersonator.',
|
||||
'default' => '',
|
||||
'example' => 'john@appwrite.io',
|
||||
])
|
||||
->addRule('userName', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'User Name.',
|
||||
'description' => 'User name of the actor recorded for this log. During impersonation, this is the original impersonator.',
|
||||
'default' => '',
|
||||
'example' => 'John Doe',
|
||||
])
|
||||
|
||||
@@ -83,8 +83,8 @@ class Row extends Any
|
||||
$document->removeAttribute('$collection');
|
||||
$document->removeAttribute('$tenant');
|
||||
|
||||
if (!$document->isEmpty() && \is_numeric($document->getAttribute('$sequence', 0))) {
|
||||
$document->setAttribute('$sequence', (int)$document->getAttribute('$sequence', 0));
|
||||
if (!$document->isEmpty()) {
|
||||
$document->setAttribute('$sequence', (string)$document->getAttribute('$sequence', ''));
|
||||
}
|
||||
|
||||
foreach ($document->getAttributes() as $column) {
|
||||
|
||||
@@ -139,6 +139,20 @@ class User extends Model
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('impersonator', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Whether the user can impersonate other users.',
|
||||
'required' => false,
|
||||
'default' => false,
|
||||
'example' => false,
|
||||
])
|
||||
->addRule('impersonatorUserId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'ID of the original actor performing the impersonation. Present only when the current request is impersonating another user. Internal audit logs attribute the action to this user, while the impersonated target is recorded only in internal audit payload data.',
|
||||
'required' => false,
|
||||
'default' => '',
|
||||
'example' => '5e5ea5c16897e',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -193,12 +193,14 @@ trait ProjectCustom
|
||||
$this->assertNotEmpty($devKey['body']);
|
||||
$this->assertNotEmpty($devKey['body']['secret']);
|
||||
|
||||
$webhook = $this->client->call(Client::METHOD_POST, '/projects/' . $project['body']['$id'] . '/webhooks', [
|
||||
$webhook = $this->client->call(Client::METHOD_POST, '/webhooks', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
|
||||
'x-appwrite-project' => 'console',
|
||||
'x-appwrite-project' => $project['body']['$id'],
|
||||
'x-appwrite-mode' => 'admin'
|
||||
], [
|
||||
'webhookId' => 'unique()',
|
||||
'name' => 'Webhook Test',
|
||||
'events' => [
|
||||
'databases.*',
|
||||
|
||||
@@ -2566,9 +2566,7 @@ trait DatabasesBase
|
||||
$this->assertEquals($document1['body']['birthDay'], '1975-06-12T12:12:55.000+00:00');
|
||||
$this->assertTrue(array_key_exists('$sequence', $document1['body']));
|
||||
|
||||
$this->getSupportForIntegerIds()
|
||||
? $this->assertIsInt($document1['body']['$sequence'])
|
||||
: $this->assertIsString($document1['body']['$sequence']);
|
||||
$this->assertIsString($document1['body']['$sequence']);
|
||||
|
||||
$this->assertEquals(201, $document2['headers']['status-code']);
|
||||
$this->assertEquals($data['moviesId'], $document2['body'][$this->getContainerIdResponseKey()]);
|
||||
@@ -2640,9 +2638,7 @@ trait DatabasesBase
|
||||
/**
|
||||
* Resubmit same document, nothing to update
|
||||
*/
|
||||
$this->getSupportForIntegerIds()
|
||||
? $this->assertIsInt($document['body']['$sequence'])
|
||||
: $this->assertIsString($document['body']['$sequence']);
|
||||
$this->assertIsString($document['body']['$sequence']);
|
||||
|
||||
$upsertData = [
|
||||
'title' => 'Thor: Ragnarok',
|
||||
|
||||
@@ -1711,6 +1711,7 @@ trait WebhooksBase
|
||||
'content-type' => 'application/json',
|
||||
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
|
||||
'x-appwrite-project' => 'console',
|
||||
'X-Appwrite-Response-Format' => '1.8.0'
|
||||
], [
|
||||
'name' => 'Webhook Test',
|
||||
'enabled' => true,
|
||||
@@ -1740,6 +1741,7 @@ trait WebhooksBase
|
||||
'content-type' => 'application/json',
|
||||
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
|
||||
'x-appwrite-project' => 'console',
|
||||
'X-Appwrite-Response-Format' => '1.8.0'
|
||||
], [
|
||||
'name' => 'Webhook Test',
|
||||
'enabled' => true,
|
||||
@@ -1779,6 +1781,7 @@ trait WebhooksBase
|
||||
'content-type' => 'application/json',
|
||||
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
|
||||
'x-appwrite-project' => 'console',
|
||||
'X-Appwrite-Response-Format' => '1.8.0'
|
||||
], [
|
||||
'name' => 'Webhook Test',
|
||||
'enabled' => true,
|
||||
@@ -1824,6 +1827,7 @@ trait WebhooksBase
|
||||
'content-type' => 'application/json',
|
||||
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
|
||||
'x-appwrite-project' => 'console',
|
||||
'X-Appwrite-Response-Format' => '1.8.0'
|
||||
]));
|
||||
|
||||
// assert that the webhook is now disabled after 10 consecutive failures
|
||||
|
||||
@@ -81,10 +81,12 @@ trait ProjectsBase
|
||||
$projectData = $this->setupProjectData();
|
||||
$id = $projectData['projectId'];
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/webhooks', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/webhooks', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-project' => $id,
|
||||
'x-appwrite-mode' => 'admin',
|
||||
], $this->getHeaders()), [
|
||||
'webhookId' => 'unique()',
|
||||
'name' => 'Webhook Test',
|
||||
'events' => ['users.*.create', 'users.*.update.email'],
|
||||
'url' => 'https://appwrite.io',
|
||||
|
||||
@@ -2743,10 +2743,12 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$data = $this->setupProjectData();
|
||||
$id = $data['projectId'];
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/webhooks', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/webhooks', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-project' => $id,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
], $this->getHeaders()), [
|
||||
'webhookId' => 'unique()',
|
||||
'name' => 'Webhook Test',
|
||||
'events' => ['users.*.create', 'users.*.update.email'],
|
||||
'url' => 'https://appwrite.io',
|
||||
@@ -2768,10 +2770,12 @@ class ProjectsConsoleClientTest extends Scope
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/webhooks', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/webhooks', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-project' => $id,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
], $this->getHeaders()), [
|
||||
'webhookId' => 'unique()',
|
||||
'name' => 'Webhook Test',
|
||||
'events' => ['account.unknown', 'users.*.update.email'],
|
||||
'url' => 'https://appwrite.io',
|
||||
@@ -2782,10 +2786,12 @@ class ProjectsConsoleClientTest extends Scope
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/webhooks', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/webhooks', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-project' => $id,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
], $this->getHeaders()), [
|
||||
'webhookId' => 'unique()',
|
||||
'name' => 'Webhook Test',
|
||||
'events' => ['users.*.create', 'users.*.update.email'],
|
||||
'url' => 'invalid://appwrite.io',
|
||||
@@ -2799,9 +2805,10 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$data = $this->setupProjectWithWebhook();
|
||||
$id = $data['projectId'];
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/webhooks', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_GET, '/webhooks', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-project' => $id,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
@@ -2819,9 +2826,10 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$id = $data['projectId'];
|
||||
$webhookId = $data['webhookId'];
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/webhooks/' . $webhookId, array_merge([
|
||||
$response = $this->client->call(Client::METHOD_GET, '/webhooks/' . $webhookId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-project' => $id,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
@@ -2837,9 +2845,10 @@ class ProjectsConsoleClientTest extends Scope
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/webhooks/error', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_GET, '/webhooks/error', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-project' => $id,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(404, $response['headers']['status-code']);
|
||||
@@ -2851,9 +2860,10 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$id = $data['projectId'];
|
||||
$webhookId = $data['webhookId'];
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/projects/' . $id . '/webhooks/' . $webhookId, array_merge([
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/webhooks/' . $webhookId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-project' => $id,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
], $this->getHeaders()), [
|
||||
'name' => 'Webhook Test Update',
|
||||
'events' => ['users.*.delete', 'users.*.sessions.*.delete', 'buckets.*.files.*.create'],
|
||||
@@ -2875,9 +2885,10 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$this->assertEquals('', $response['body']['httpUser']);
|
||||
$this->assertEquals('', $response['body']['httpPass']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/webhooks/' . $webhookId, array_merge([
|
||||
$response = $this->client->call(Client::METHOD_GET, '/webhooks/' . $webhookId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-project' => $id,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
@@ -2897,9 +2908,10 @@ class ProjectsConsoleClientTest extends Scope
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/projects/' . $id . '/webhooks/' . $webhookId, array_merge([
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/webhooks/' . $webhookId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-project' => $id,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
], $this->getHeaders()), [
|
||||
'name' => 'Webhook Test Update',
|
||||
'events' => ['users.*.delete', 'users.*.sessions.*.delete', 'buckets.*.files.*.unknown'],
|
||||
@@ -2909,9 +2921,10 @@ class ProjectsConsoleClientTest extends Scope
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/projects/' . $id . '/webhooks/' . $webhookId, array_merge([
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/webhooks/' . $webhookId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-project' => $id,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
], $this->getHeaders()), [
|
||||
'name' => 'Webhook Test Update',
|
||||
'events' => ['users.*.delete', 'users.*.sessions.*.delete', 'buckets.*.files.*.create'],
|
||||
@@ -2921,9 +2934,10 @@ class ProjectsConsoleClientTest extends Scope
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/projects/' . $id . '/webhooks/' . $webhookId, array_merge([
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/webhooks/' . $webhookId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-project' => $id,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
], $this->getHeaders()), [
|
||||
'name' => 'Webhook Test Update',
|
||||
'events' => ['users.*.delete', 'users.*.sessions.*.delete', 'buckets.*.files.*.create'],
|
||||
@@ -2940,9 +2954,10 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$webhookId = $data['webhookId'];
|
||||
$signatureKey = $data['signatureKey'];
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/webhooks/' . $webhookId . '/signature', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/webhooks/' . $webhookId . '/signature', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-project' => $id,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
@@ -2957,10 +2972,12 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$id = $projectData['projectId'];
|
||||
|
||||
// Create a webhook to delete
|
||||
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/webhooks', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/webhooks', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-project' => $id,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
], $this->getHeaders()), [
|
||||
'webhookId' => 'unique()',
|
||||
'name' => 'Webhook To Delete',
|
||||
'events' => ['users.*.create'],
|
||||
'url' => 'https://appwrite.io',
|
||||
@@ -2972,17 +2989,19 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
$webhookId = $response['body']['$id'];
|
||||
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $id . '/webhooks/' . $webhookId, array_merge([
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/webhooks/' . $webhookId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-project' => $id,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
$this->assertEmpty($response['body']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/webhooks/' . $webhookId, array_merge([
|
||||
$response = $this->client->call(Client::METHOD_GET, '/webhooks/' . $webhookId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-project' => $id,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(404, $response['headers']['status-code']);
|
||||
@@ -2990,9 +3009,10 @@ class ProjectsConsoleClientTest extends Scope
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $id . '/webhooks/error', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/webhooks/error', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-project' => $id,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(404, $response['headers']['status-code']);
|
||||
@@ -4350,9 +4370,10 @@ class ProjectsConsoleClientTest extends Scope
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $id . '/webhooks/error', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/webhooks/error', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-project' => $id,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(404, $response['headers']['status-code']);
|
||||
|
||||
@@ -2237,4 +2237,491 @@ trait UsersBase
|
||||
|
||||
// TODO add test for session delete
|
||||
// TODO add test for all sessions delete
|
||||
|
||||
/**
|
||||
* Test PATCH /users/:userId/impersonator - set and unset impersonator capability
|
||||
*/
|
||||
public function testUpdateUserImpersonator(): void
|
||||
{
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$headers = array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], $this->getHeaders());
|
||||
|
||||
$user = $this->client->call(Client::METHOD_POST, '/users', $headers, [
|
||||
'userId' => ID::unique(),
|
||||
'email' => 'impersonator-update-test@appwrite.io',
|
||||
'password' => 'password',
|
||||
'name' => 'Impersonator Update Test',
|
||||
]);
|
||||
$this->assertEquals(201, $user['headers']['status-code']);
|
||||
$userId = $user['body']['$id'];
|
||||
|
||||
$this->assertFalse($user['body']['impersonator'] ?? false);
|
||||
|
||||
$updated = $this->client->call(Client::METHOD_PATCH, '/users/' . $userId . '/impersonator', $headers, [
|
||||
'impersonator' => true,
|
||||
]);
|
||||
$this->assertEquals(200, $updated['headers']['status-code']);
|
||||
$this->assertTrue($updated['body']['impersonator']);
|
||||
$this->assertEquals($userId, $updated['body']['$id']);
|
||||
|
||||
$get = $this->client->call(Client::METHOD_GET, '/users/' . $userId, $headers);
|
||||
$this->assertEquals(200, $get['headers']['status-code']);
|
||||
$this->assertTrue($get['body']['impersonator']);
|
||||
|
||||
$updated = $this->client->call(Client::METHOD_PATCH, '/users/' . $userId . '/impersonator', $headers, [
|
||||
'impersonator' => false,
|
||||
]);
|
||||
$this->assertEquals(200, $updated['headers']['status-code']);
|
||||
$this->assertFalse($updated['body']['impersonator']);
|
||||
|
||||
$get = $this->client->call(Client::METHOD_GET, '/users/' . $userId, $headers);
|
||||
$this->assertEquals(200, $get['headers']['status-code']);
|
||||
$this->assertFalse($get['body']['impersonator']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test impersonation by user ID: session auth + header → act as target user, account returns target + impersonatorUserId
|
||||
*/
|
||||
public function testImpersonateByUserId(): void
|
||||
{
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$headers = array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], $this->getHeaders());
|
||||
|
||||
$userA = $this->client->call(Client::METHOD_POST, '/users', $headers, [
|
||||
'userId' => ID::unique(),
|
||||
'email' => 'impersonator-a@appwrite.io',
|
||||
'password' => 'password',
|
||||
'name' => 'User A Impersonator',
|
||||
]);
|
||||
$this->assertEquals(201, $userA['headers']['status-code']);
|
||||
$idA = $userA['body']['$id'];
|
||||
|
||||
$userB = $this->client->call(Client::METHOD_POST, '/users', $headers, [
|
||||
'userId' => ID::unique(),
|
||||
'email' => 'impersonator-target-b@appwrite.io',
|
||||
'password' => 'password',
|
||||
'name' => 'User B Target',
|
||||
]);
|
||||
$this->assertEquals(201, $userB['headers']['status-code']);
|
||||
$idB = $userB['body']['$id'];
|
||||
|
||||
$patch = $this->client->call(Client::METHOD_PATCH, '/users/' . $idA . '/impersonator', $headers, ['impersonator' => true]);
|
||||
$this->assertEquals(200, $patch['headers']['status-code']);
|
||||
|
||||
$session = $this->client->call(Client::METHOD_POST, '/users/' . $idA . '/sessions', $headers);
|
||||
$this->assertEquals(201, $session['headers']['status-code']);
|
||||
$sessionSecret = $session['body']['secret'];
|
||||
|
||||
$accountHeaders = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-session' => $sessionSecret,
|
||||
'x-appwrite-impersonate-user-id' => $idB,
|
||||
];
|
||||
|
||||
$account = $this->client->call(Client::METHOD_GET, '/account', $accountHeaders);
|
||||
$this->assertEquals(200, $account['headers']['status-code']);
|
||||
$this->assertEquals($idB, $account['body']['$id']);
|
||||
$this->assertEquals('User B Target', $account['body']['name']);
|
||||
$this->assertEquals($idA, $account['body']['impersonatorUserId']);
|
||||
|
||||
$withoutHeader = $this->client->call(Client::METHOD_GET, '/account', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-session' => $sessionSecret,
|
||||
]);
|
||||
$this->assertEquals(200, $withoutHeader['headers']['status-code']);
|
||||
$this->assertEquals($idA, $withoutHeader['body']['$id']);
|
||||
$this->assertEquals('User A Impersonator', $withoutHeader['body']['name']);
|
||||
$this->assertArrayHasKey('impersonatorUserId', $withoutHeader['body']);
|
||||
$this->assertNull($withoutHeader['body']['impersonatorUserId']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test impersonation by email header
|
||||
*/
|
||||
public function testImpersonateByEmail(): void
|
||||
{
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$headers = array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], $this->getHeaders());
|
||||
|
||||
$userA = $this->client->call(Client::METHOD_POST, '/users', $headers, [
|
||||
'userId' => ID::unique(),
|
||||
'email' => 'impersonate-by-email-actor@appwrite.io',
|
||||
'password' => 'password',
|
||||
'name' => 'Actor',
|
||||
]);
|
||||
$this->assertEquals(201, $userA['headers']['status-code']);
|
||||
$idA = $userA['body']['$id'];
|
||||
|
||||
$targetEmail = 'impersonate-by-email-target@appwrite.io';
|
||||
$userB = $this->client->call(Client::METHOD_POST, '/users', $headers, [
|
||||
'userId' => ID::unique(),
|
||||
'email' => $targetEmail,
|
||||
'password' => 'password',
|
||||
'name' => 'Target By Email',
|
||||
]);
|
||||
$this->assertEquals(201, $userB['headers']['status-code']);
|
||||
$idB = $userB['body']['$id'];
|
||||
|
||||
$this->client->call(Client::METHOD_PATCH, '/users/' . $idA . '/impersonator', $headers, ['impersonator' => true]);
|
||||
$session = $this->client->call(Client::METHOD_POST, '/users/' . $idA . '/sessions', $headers);
|
||||
$this->assertEquals(201, $session['headers']['status-code']);
|
||||
|
||||
$account = $this->client->call(Client::METHOD_GET, '/account', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-session' => $session['body']['secret'],
|
||||
'x-appwrite-impersonate-user-email' => $targetEmail,
|
||||
]);
|
||||
$this->assertEquals(200, $account['headers']['status-code']);
|
||||
$this->assertEquals($idB, $account['body']['$id']);
|
||||
$this->assertEquals($idA, $account['body']['impersonatorUserId']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test impersonation by phone header
|
||||
*/
|
||||
public function testImpersonateByPhone(): void
|
||||
{
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$headers = array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], $this->getHeaders());
|
||||
|
||||
$userA = $this->client->call(Client::METHOD_POST, '/users', $headers, [
|
||||
'userId' => ID::unique(),
|
||||
'email' => 'impersonate-by-phone-actor@appwrite.io',
|
||||
'password' => 'password',
|
||||
'name' => 'Actor Phone',
|
||||
]);
|
||||
$this->assertEquals(201, $userA['headers']['status-code']);
|
||||
$idA = $userA['body']['$id'];
|
||||
|
||||
$targetPhone = '+1555010200';
|
||||
$userB = $this->client->call(Client::METHOD_POST, '/users', $headers, [
|
||||
'userId' => ID::unique(),
|
||||
'phone' => $targetPhone,
|
||||
'name' => 'Target By Phone',
|
||||
]);
|
||||
$this->assertEquals(201, $userB['headers']['status-code']);
|
||||
$idB = $userB['body']['$id'];
|
||||
|
||||
$this->client->call(Client::METHOD_PATCH, '/users/' . $idA . '/impersonator', $headers, ['impersonator' => true]);
|
||||
$session = $this->client->call(Client::METHOD_POST, '/users/' . $idA . '/sessions', $headers);
|
||||
$this->assertEquals(201, $session['headers']['status-code']);
|
||||
|
||||
$account = $this->client->call(Client::METHOD_GET, '/account', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-session' => $session['body']['secret'],
|
||||
'x-appwrite-impersonate-user-phone' => $targetPhone,
|
||||
]);
|
||||
$this->assertEquals(200, $account['headers']['status-code']);
|
||||
$this->assertEquals($idB, $account['body']['$id']);
|
||||
$this->assertEquals($idA, $account['body']['impersonatorUserId']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that user without impersonator capability does not get swapped when sending impersonation header
|
||||
*/
|
||||
public function testImpersonationRequiresCapability(): void
|
||||
{
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$headers = array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], $this->getHeaders());
|
||||
|
||||
$userA = $this->client->call(Client::METHOD_POST, '/users', $headers, [
|
||||
'userId' => ID::unique(),
|
||||
'email' => 'non-impersonator@appwrite.io',
|
||||
'password' => 'password',
|
||||
'name' => 'Non Impersonator',
|
||||
]);
|
||||
$this->assertEquals(201, $userA['headers']['status-code']);
|
||||
$idA = $userA['body']['$id'];
|
||||
|
||||
$userB = $this->client->call(Client::METHOD_POST, '/users', $headers, [
|
||||
'userId' => ID::unique(),
|
||||
'email' => 'other-user@appwrite.io',
|
||||
'password' => 'password',
|
||||
'name' => 'Other User',
|
||||
]);
|
||||
$this->assertEquals(201, $userB['headers']['status-code']);
|
||||
$idB = $userB['body']['$id'];
|
||||
|
||||
$session = $this->client->call(Client::METHOD_POST, '/users/' . $idA . '/sessions', $headers);
|
||||
$this->assertEquals(201, $session['headers']['status-code']);
|
||||
|
||||
$account = $this->client->call(Client::METHOD_GET, '/account', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-session' => $session['body']['secret'],
|
||||
'x-appwrite-impersonate-user-id' => $idB,
|
||||
]);
|
||||
$this->assertEquals(200, $account['headers']['status-code']);
|
||||
$this->assertEquals($idA, $account['body']['$id']);
|
||||
$this->assertEquals('Non Impersonator', $account['body']['name']);
|
||||
$this->assertArrayHasKey('impersonatorUserId', $account['body']);
|
||||
$this->assertNull($account['body']['impersonatorUserId']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a normal user does not gain users.read while sending impersonation headers
|
||||
*/
|
||||
public function testImpersonationUsersReadScopeRequiresCapability(): void
|
||||
{
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$headers = array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], $this->getHeaders());
|
||||
|
||||
$userA = $this->client->call(Client::METHOD_POST, '/users', $headers, [
|
||||
'userId' => ID::unique(),
|
||||
'email' => 'non-impersonator-scope@appwrite.io',
|
||||
'password' => 'password',
|
||||
'name' => 'Non Impersonator Scope',
|
||||
]);
|
||||
$this->assertEquals(201, $userA['headers']['status-code']);
|
||||
$idA = $userA['body']['$id'];
|
||||
|
||||
$userB = $this->client->call(Client::METHOD_POST, '/users', $headers, [
|
||||
'userId' => ID::unique(),
|
||||
'email' => 'non-impersonator-scope-target@appwrite.io',
|
||||
'password' => 'password',
|
||||
'name' => 'Non Impersonator Scope Target',
|
||||
]);
|
||||
$this->assertEquals(201, $userB['headers']['status-code']);
|
||||
|
||||
$session = $this->client->call(Client::METHOD_POST, '/users/' . $idA . '/sessions', $headers);
|
||||
$this->assertEquals(201, $session['headers']['status-code']);
|
||||
|
||||
$users = $this->client->call(Client::METHOD_GET, '/users', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-session' => $session['body']['secret'],
|
||||
'x-appwrite-impersonate-user-id' => $userB['body']['$id'],
|
||||
]);
|
||||
$this->assertEquals(401, $users['headers']['status-code']);
|
||||
$this->assertEquals('general_unauthorized_scope', $users['body']['type']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that email and phone impersonation headers are ignored for users without impersonator capability
|
||||
*/
|
||||
public function testImpersonationByEmailAndPhoneRequireCapability(): void
|
||||
{
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$headers = array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], $this->getHeaders());
|
||||
|
||||
$userA = $this->client->call(Client::METHOD_POST, '/users', $headers, [
|
||||
'userId' => ID::unique(),
|
||||
'email' => 'non-impersonator-headers@appwrite.io',
|
||||
'password' => 'password',
|
||||
'name' => 'Non Impersonator Headers',
|
||||
]);
|
||||
$this->assertEquals(201, $userA['headers']['status-code']);
|
||||
$idA = $userA['body']['$id'];
|
||||
|
||||
$targetEmail = 'non-impersonator-target-email@appwrite.io';
|
||||
$targetByEmail = $this->client->call(Client::METHOD_POST, '/users', $headers, [
|
||||
'userId' => ID::unique(),
|
||||
'email' => $targetEmail,
|
||||
'password' => 'password',
|
||||
'name' => 'Target Email',
|
||||
]);
|
||||
$this->assertEquals(201, $targetByEmail['headers']['status-code']);
|
||||
|
||||
$targetPhone = '+1555010300';
|
||||
$targetByPhone = $this->client->call(Client::METHOD_POST, '/users', $headers, [
|
||||
'userId' => ID::unique(),
|
||||
'phone' => $targetPhone,
|
||||
'name' => 'Target Phone',
|
||||
]);
|
||||
$this->assertEquals(201, $targetByPhone['headers']['status-code']);
|
||||
|
||||
$session = $this->client->call(Client::METHOD_POST, '/users/' . $idA . '/sessions', $headers);
|
||||
$this->assertEquals(201, $session['headers']['status-code']);
|
||||
|
||||
$accountByEmail = $this->client->call(Client::METHOD_GET, '/account', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-session' => $session['body']['secret'],
|
||||
'x-appwrite-impersonate-user-email' => $targetEmail,
|
||||
]);
|
||||
$this->assertEquals(200, $accountByEmail['headers']['status-code']);
|
||||
$this->assertEquals($idA, $accountByEmail['body']['$id']);
|
||||
$this->assertEquals('Non Impersonator Headers', $accountByEmail['body']['name']);
|
||||
$this->assertArrayHasKey('impersonatorUserId', $accountByEmail['body']);
|
||||
$this->assertNull($accountByEmail['body']['impersonatorUserId']);
|
||||
|
||||
$accountByPhone = $this->client->call(Client::METHOD_GET, '/account', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-session' => $session['body']['secret'],
|
||||
'x-appwrite-impersonate-user-phone' => $targetPhone,
|
||||
]);
|
||||
$this->assertEquals(200, $accountByPhone['headers']['status-code']);
|
||||
$this->assertEquals($idA, $accountByPhone['body']['$id']);
|
||||
$this->assertEquals('Non Impersonator Headers', $accountByPhone['body']['name']);
|
||||
$this->assertArrayHasKey('impersonatorUserId', $accountByPhone['body']);
|
||||
$this->assertNull($accountByPhone['body']['impersonatorUserId']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that impersonator users get users.read before selecting a target
|
||||
*/
|
||||
public function testImpersonatorUsersReadScopeWithoutActiveImpersonation(): void
|
||||
{
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$headers = array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], $this->getHeaders());
|
||||
|
||||
$user = $this->client->call(Client::METHOD_POST, '/users', $headers, [
|
||||
'userId' => ID::unique(),
|
||||
'email' => 'impersonator-browse-users@appwrite.io',
|
||||
'password' => 'password',
|
||||
'name' => 'Impersonator Browse Users',
|
||||
]);
|
||||
$this->assertEquals(201, $user['headers']['status-code']);
|
||||
|
||||
$patch = $this->client->call(
|
||||
Client::METHOD_PATCH,
|
||||
'/users/' . $user['body']['$id'] . '/impersonator',
|
||||
$headers,
|
||||
['impersonator' => true]
|
||||
);
|
||||
$this->assertEquals(200, $patch['headers']['status-code']);
|
||||
|
||||
$session = $this->client->call(Client::METHOD_POST, '/users/' . $user['body']['$id'] . '/sessions', $headers);
|
||||
$this->assertEquals(201, $session['headers']['status-code']);
|
||||
|
||||
$users = $this->client->call(Client::METHOD_GET, '/users', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-session' => $session['body']['secret'],
|
||||
]);
|
||||
$this->assertEquals(200, $users['headers']['status-code']);
|
||||
$this->assertIsArray($users['body']['users']);
|
||||
$this->assertGreaterThanOrEqual(1, count($users['body']['users']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that when impersonating, users.read scope is granted and list users succeeds
|
||||
*/
|
||||
public function testImpersonationUsersReadScope(): void
|
||||
{
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$headers = array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], $this->getHeaders());
|
||||
|
||||
$userA = $this->client->call(Client::METHOD_POST, '/users', $headers, [
|
||||
'userId' => ID::unique(),
|
||||
'email' => 'impersonator-list-test@appwrite.io',
|
||||
'password' => 'password',
|
||||
'name' => 'Impersonator List Test',
|
||||
]);
|
||||
$this->assertEquals(201, $userA['headers']['status-code']);
|
||||
$idA = $userA['body']['$id'];
|
||||
|
||||
$userB = $this->client->call(Client::METHOD_POST, '/users', $headers, [
|
||||
'userId' => ID::unique(),
|
||||
'email' => 'impersonator-list-target@appwrite.io',
|
||||
'password' => 'password',
|
||||
'name' => 'List Target',
|
||||
]);
|
||||
$this->assertEquals(201, $userB['headers']['status-code']);
|
||||
$idB = $userB['body']['$id'];
|
||||
|
||||
$this->client->call(Client::METHOD_PATCH, '/users/' . $idA . '/impersonator', $headers, ['impersonator' => true]);
|
||||
$session = $this->client->call(Client::METHOD_POST, '/users/' . $idA . '/sessions', $headers);
|
||||
$this->assertEquals(201, $session['headers']['status-code']);
|
||||
|
||||
$listHeaders = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-session' => $session['body']['secret'],
|
||||
'x-appwrite-impersonate-user-id' => $idB,
|
||||
];
|
||||
|
||||
$users = $this->client->call(Client::METHOD_GET, '/users', $listHeaders);
|
||||
$this->assertEquals(200, $users['headers']['status-code']);
|
||||
$this->assertIsArray($users['body']['users']);
|
||||
$this->assertGreaterThanOrEqual(2, count($users['body']['users']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test list users with query filter on impersonator attribute
|
||||
*/
|
||||
public function testListUsersFilterByImpersonator(): void
|
||||
{
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$headers = array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], $this->getHeaders());
|
||||
|
||||
$userWithImpersonator = $this->client->call(Client::METHOD_POST, '/users', $headers, [
|
||||
'userId' => ID::unique(),
|
||||
'email' => 'filter-impersonator-true@appwrite.io',
|
||||
'password' => 'password',
|
||||
'name' => 'Has Impersonator',
|
||||
]);
|
||||
$this->assertEquals(201, $userWithImpersonator['headers']['status-code']);
|
||||
$this->client->call(Client::METHOD_PATCH, '/users/' . $userWithImpersonator['body']['$id'] . '/impersonator', $headers, ['impersonator' => true]);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/users', $headers, [
|
||||
'queries' => [
|
||||
Query::equal('impersonator', [true])->toString(),
|
||||
],
|
||||
]);
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertIsArray($response['body']['users']);
|
||||
$userIds = array_column($response['body']['users'], '$id');
|
||||
$this->assertContains($userWithImpersonator['body']['$id'], $userIds);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/users', $headers, [
|
||||
'queries' => [
|
||||
Query::equal('impersonator', [false])->toString(),
|
||||
],
|
||||
]);
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertIsArray($response['body']['users']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test PATCH /users/:userId/impersonator for non-existent user returns 404
|
||||
*/
|
||||
public function testUpdateUserImpersonatorNotFound(): void
|
||||
{
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$headers = array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], $this->getHeaders());
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/users/nonexistentuserid123/impersonator', $headers, [
|
||||
'impersonator' => true,
|
||||
]);
|
||||
$this->assertEquals(404, $response['headers']['status-code']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Utopia PHP Framework
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Tests
|
||||
*
|
||||
* @link https://github.com/utopia-php/framework
|
||||
* @author Appwrite Team <team@appwrite.io>
|
||||
* @version 1.0 RC4
|
||||
* @license The MIT License (MIT) <http://www.opensource.org/licenses/mit-license.php>
|
||||
*/
|
||||
|
||||
namespace Tests\Unit\Network\Validators;
|
||||
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class EmailTest extends TestCase
|
||||
{
|
||||
protected ?Email $email = null;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->email = new Email();
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
$this->email = null;
|
||||
}
|
||||
|
||||
public function testIsValid(): void
|
||||
{
|
||||
$this->assertEquals(true, $this->email->isValid('email@domain.com'));
|
||||
$this->assertEquals(true, $this->email->isValid('firstname.lastname@domain.com'));
|
||||
$this->assertEquals(true, $this->email->isValid('email@subdomain.domain.com'));
|
||||
$this->assertEquals(true, $this->email->isValid('firstname+lastname@domain.com'));
|
||||
$this->assertEquals(true, $this->email->isValid('email@[123.123.123.123]'));
|
||||
$this->assertEquals(true, $this->email->isValid('"email"@domain.com'));
|
||||
$this->assertEquals(true, $this->email->isValid('1234567890@domain.com'));
|
||||
$this->assertEquals(true, $this->email->isValid('email@domain-one.com'));
|
||||
$this->assertEquals(true, $this->email->isValid('_______@domain.com'));
|
||||
$this->assertEquals(true, $this->email->isValid('email@domain.name'));
|
||||
$this->assertEquals(true, $this->email->isValid('email@domain.co.jp'));
|
||||
$this->assertEquals(true, $this->email->isValid('firstname-lastname@domain.com'));
|
||||
$this->assertEquals(false, $this->email->isValid(false));
|
||||
$this->assertEquals(false, $this->email->isValid(['string', 'string']));
|
||||
$this->assertEquals(false, $this->email->isValid(1));
|
||||
$this->assertEquals(false, $this->email->isValid(1.2));
|
||||
$this->assertEquals(false, $this->email->isValid('plainaddress')); // Missing @ sign and domain
|
||||
$this->assertEquals(false, $this->email->isValid('@domain.com')); // Missing username
|
||||
$this->assertEquals(false, $this->email->isValid('#@%^%#$@#$@#.com')); // Garbage
|
||||
$this->assertEquals(false, $this->email->isValid('Joe Smith <email@domain.com>')); // Encoded html within email is invalid
|
||||
$this->assertEquals(false, $this->email->isValid('email.domain.com')); // Missing @
|
||||
$this->assertEquals(false, $this->email->isValid('email@domain@domain.com')); // Two @ sign
|
||||
$this->assertEquals(false, $this->email->isValid('.email@domain.com')); // Leading dot in address is not allowed
|
||||
$this->assertEquals(false, $this->email->isValid('email.@domain.com')); // Trailing dot in address is not allowed
|
||||
$this->assertEquals(false, $this->email->isValid('email..email@domain.com')); // Multiple dots
|
||||
$this->assertEquals(false, $this->email->isValid('あいうえお@domain.com')); // Unicode char as address
|
||||
$this->assertEquals(false, $this->email->isValid('email@domain.com (Joe Smith)')); // Text followed email is not allowed
|
||||
$this->assertEquals(false, $this->email->isValid('email@domain')); // Missing top level domain (.com/.net/.org/etc)
|
||||
$this->assertEquals(false, $this->email->isValid('email@-domain.com')); // Leading dash in front of domain is invalid
|
||||
$this->assertEquals(false, $this->email->isValid('email@111.222.333.44444')); // Invalid IP format
|
||||
$this->assertEquals(false, $this->email->isValid('email@domain..com')); // Multiple dot in the domain portion is invalid
|
||||
$this->assertEquals($this->email->getType(), 'string');
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,10 @@ class OriginTest extends TestCase
|
||||
$this->assertEquals(false, $validator->isValid('ms-browser-extension://com.company.appname'));
|
||||
$this->assertEquals('Invalid Origin. Register your new client (com.company.appname) as a new Web (Edge Extension) platform on your project console dashboard', $validator->getDescription());
|
||||
|
||||
$this->assertEquals(true, $validator->isValid('tauri://localhost'));
|
||||
$this->assertEquals(false, $validator->isValid('tauri://example.com'));
|
||||
$this->assertEquals('Invalid Origin. Register your new client (example.com) as a new Web (Tauri) platform on your project console dashboard', $validator->getDescription());
|
||||
|
||||
$this->assertEquals(false, $validator->isValid('random-scheme://localhost'));
|
||||
$this->assertEquals('Invalid Scheme. The scheme used (random-scheme) in the Origin (random-scheme://localhost) is not supported. If you are using a custom scheme, please change it to `appwrite-callback-<PROJECT_ID>`', $validator->getDescription());
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ class ConfigTest extends TestCase
|
||||
$this->assertFalse($config->isLocal());
|
||||
$this->assertNull($config->getHostPath());
|
||||
$this->assertNull($config->getLockedDatabase());
|
||||
$this->assertEquals(['mongodb', 'mariadb'], $config->getEnabledDatabases());
|
||||
$this->assertEmpty($config->getVars());
|
||||
}
|
||||
|
||||
@@ -412,6 +413,7 @@ class ConfigTest extends TestCase
|
||||
'isLocal',
|
||||
'hostPath',
|
||||
'lockedDatabase',
|
||||
'enabledDatabases',
|
||||
];
|
||||
|
||||
foreach ($expectedKeys as $key) {
|
||||
@@ -435,6 +437,7 @@ class ConfigTest extends TestCase
|
||||
$this->assertFalse($array['isLocal']);
|
||||
$this->assertNull($array['hostPath']);
|
||||
$this->assertNull($array['lockedDatabase']);
|
||||
$this->assertEquals(['mongodb', 'mariadb'], $array['enabledDatabases']);
|
||||
}
|
||||
|
||||
public function testMultipleApplyCallsAccumulate(): void
|
||||
@@ -485,4 +488,67 @@ class ConfigTest extends TestCase
|
||||
$this->assertCount(1, $vars);
|
||||
$this->assertEquals('B', $vars[0]['name']);
|
||||
}
|
||||
|
||||
public function testDefaultEnabledDatabases(): void
|
||||
{
|
||||
$config = new Config();
|
||||
$this->assertEquals(['mongodb', 'mariadb'], $config->getEnabledDatabases());
|
||||
$this->assertTrue($config->isDatabaseEnabled('mongodb'));
|
||||
$this->assertTrue($config->isDatabaseEnabled('mariadb'));
|
||||
$this->assertFalse($config->isDatabaseEnabled('postgresql'));
|
||||
}
|
||||
|
||||
public function testSetEnabledDatabases(): void
|
||||
{
|
||||
$config = new Config();
|
||||
$config->setEnabledDatabases(['mongodb', 'mariadb', 'postgresql']);
|
||||
$this->assertEquals(['mongodb', 'mariadb', 'postgresql'], $config->getEnabledDatabases());
|
||||
$this->assertTrue($config->isDatabaseEnabled('postgresql'));
|
||||
}
|
||||
|
||||
public function testSetEnabledDatabasesFiltersInvalid(): void
|
||||
{
|
||||
$config = new Config();
|
||||
$config->setEnabledDatabases(['mongodb', '', null, 42, 'mariadb']);
|
||||
$this->assertEquals(['mongodb', 'mariadb'], $config->getEnabledDatabases());
|
||||
}
|
||||
|
||||
public function testSetEnabledDatabasesEmptyArrayPreservesExisting(): void
|
||||
{
|
||||
$config = new Config();
|
||||
$config->setEnabledDatabases(['mongodb', 'postgresql']);
|
||||
$config->setEnabledDatabases([]);
|
||||
$this->assertEquals(['mongodb', 'postgresql'], $config->getEnabledDatabases());
|
||||
}
|
||||
|
||||
public function testApplyEnabledDatabases(): void
|
||||
{
|
||||
$config = new Config();
|
||||
$config->apply(['enabledDatabases' => ['mongodb', 'mariadb', 'postgresql']]);
|
||||
$this->assertEquals(['mongodb', 'mariadb', 'postgresql'], $config->getEnabledDatabases());
|
||||
$this->assertTrue($config->isDatabaseEnabled('postgresql'));
|
||||
}
|
||||
|
||||
public function testApplyEnabledDatabasesNonArrayIgnored(): void
|
||||
{
|
||||
$config = new Config();
|
||||
$config->apply(['enabledDatabases' => 'mongodb']);
|
||||
$this->assertEquals(['mongodb', 'mariadb'], $config->getEnabledDatabases());
|
||||
}
|
||||
|
||||
public function testEnabledDatabasesInToArray(): void
|
||||
{
|
||||
$config = new Config();
|
||||
$config->setEnabledDatabases(['mongodb']);
|
||||
$array = $config->toArray();
|
||||
$this->assertEquals(['mongodb'], $array['enabledDatabases']);
|
||||
}
|
||||
|
||||
public function testEnabledDatabasesRoundTrip(): void
|
||||
{
|
||||
$config = new Config();
|
||||
$config->setEnabledDatabases(['mongodb', 'postgresql']);
|
||||
$rebuilt = new Config($config->toArray());
|
||||
$this->assertEquals(['mongodb', 'postgresql'], $rebuilt->getEnabledDatabases());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Utopia\Request\Filters;
|
||||
|
||||
use Appwrite\Utopia\Request\Filter;
|
||||
use Appwrite\Utopia\Request\Filters\V21;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class V21Test extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var Filter
|
||||
*/
|
||||
protected $filter;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->filter = new V21();
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
}
|
||||
|
||||
public static function functionsCreateTemplateDeploymentProvider(): array
|
||||
{
|
||||
return [
|
||||
'convert version to type and reference' => [
|
||||
[
|
||||
'templateId' => 'template123',
|
||||
'version' => '1.0.0',
|
||||
],
|
||||
[
|
||||
'templateId' => 'template123',
|
||||
'type' => 'tag',
|
||||
'reference' => '1.0.0',
|
||||
]
|
||||
],
|
||||
'handle version with semver string' => [
|
||||
[
|
||||
'version' => 'v2.3.1',
|
||||
'templateId' => 'template456',
|
||||
],
|
||||
[
|
||||
'type' => 'tag',
|
||||
'reference' => 'v2.3.1',
|
||||
'templateId' => 'template456',
|
||||
]
|
||||
],
|
||||
'skip conversion when version is empty string' => [
|
||||
[
|
||||
'templateId' => 'template123',
|
||||
'version' => '',
|
||||
],
|
||||
[
|
||||
'templateId' => 'template123',
|
||||
'version' => '',
|
||||
]
|
||||
],
|
||||
'skip conversion when version is missing' => [
|
||||
[
|
||||
'templateId' => 'template123',
|
||||
],
|
||||
[
|
||||
'templateId' => 'template123',
|
||||
]
|
||||
],
|
||||
'preserve other fields when converting version' => [
|
||||
[
|
||||
'templateId' => 'template123',
|
||||
'version' => '3.0.0',
|
||||
'activate' => true,
|
||||
'buildCommand' => 'npm run build',
|
||||
],
|
||||
[
|
||||
'templateId' => 'template123',
|
||||
'type' => 'tag',
|
||||
'reference' => '3.0.0',
|
||||
'activate' => true,
|
||||
'buildCommand' => 'npm run build',
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('functionsCreateTemplateDeploymentProvider')]
|
||||
public function testFunctionsCreateTemplateDeployment(array $content, array $expected): void
|
||||
{
|
||||
$model = 'functions.createTemplateDeployment';
|
||||
|
||||
$result = $this->filter->parse($content, $model);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public static function sitesCreateTemplateDeploymentProvider(): array
|
||||
{
|
||||
return [
|
||||
'convert version to type and reference' => [
|
||||
[
|
||||
'templateId' => 'site-template123',
|
||||
'version' => '2.0.0',
|
||||
],
|
||||
[
|
||||
'templateId' => 'site-template123',
|
||||
'type' => 'tag',
|
||||
'reference' => '2.0.0',
|
||||
]
|
||||
],
|
||||
'skip conversion when version is empty' => [
|
||||
[
|
||||
'templateId' => 'site-template123',
|
||||
'version' => '',
|
||||
],
|
||||
[
|
||||
'templateId' => 'site-template123',
|
||||
'version' => '',
|
||||
]
|
||||
],
|
||||
'skip conversion when version is missing' => [
|
||||
[
|
||||
'templateId' => 'site-template123',
|
||||
],
|
||||
[
|
||||
'templateId' => 'site-template123',
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('sitesCreateTemplateDeploymentProvider')]
|
||||
public function testSitesCreateTemplateDeployment(array $content, array $expected): void
|
||||
{
|
||||
$model = 'sites.createTemplateDeployment';
|
||||
|
||||
$result = $this->filter->parse($content, $model);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public static function functionsCreateProvider(): array
|
||||
{
|
||||
return [
|
||||
'convert specification to buildSpecification and runtimeSpecification' => [
|
||||
[
|
||||
'name' => 'test-function',
|
||||
'runtime' => 'node-18.0',
|
||||
'specification' => 's-1vcpu-512mb',
|
||||
],
|
||||
[
|
||||
'name' => 'test-function',
|
||||
'runtime' => 'node-18.0',
|
||||
'buildSpecification' => 's-1vcpu-512mb',
|
||||
'runtimeSpecification' => 's-1vcpu-512mb',
|
||||
]
|
||||
],
|
||||
'skip conversion when specification is empty' => [
|
||||
[
|
||||
'name' => 'test-function',
|
||||
'runtime' => 'node-18.0',
|
||||
'specification' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'test-function',
|
||||
'runtime' => 'node-18.0',
|
||||
'specification' => '',
|
||||
]
|
||||
],
|
||||
'skip conversion when specification is missing' => [
|
||||
[
|
||||
'name' => 'test-function',
|
||||
'runtime' => 'node-18.0',
|
||||
],
|
||||
[
|
||||
'name' => 'test-function',
|
||||
'runtime' => 'node-18.0',
|
||||
]
|
||||
],
|
||||
'preserve other fields when converting specification' => [
|
||||
[
|
||||
'name' => 'test-function',
|
||||
'specification' => 's-2vcpu-1gb',
|
||||
'timeout' => 30,
|
||||
'enabled' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'test-function',
|
||||
'buildSpecification' => 's-2vcpu-1gb',
|
||||
'runtimeSpecification' => 's-2vcpu-1gb',
|
||||
'timeout' => 30,
|
||||
'enabled' => true,
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('functionsCreateProvider')]
|
||||
public function testFunctionsCreate(array $content, array $expected): void
|
||||
{
|
||||
$model = 'functions.create';
|
||||
|
||||
$result = $this->filter->parse($content, $model);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public static function sitesCreateProvider(): array
|
||||
{
|
||||
return [
|
||||
'convert specification to buildSpecification and runtimeSpecification' => [
|
||||
[
|
||||
'name' => 'test-site',
|
||||
'framework' => 'nextjs',
|
||||
'specification' => 's-1vcpu-512mb',
|
||||
],
|
||||
[
|
||||
'name' => 'test-site',
|
||||
'framework' => 'nextjs',
|
||||
'buildSpecification' => 's-1vcpu-512mb',
|
||||
'runtimeSpecification' => 's-1vcpu-512mb',
|
||||
]
|
||||
],
|
||||
'skip conversion when specification is empty' => [
|
||||
[
|
||||
'name' => 'test-site',
|
||||
'specification' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'test-site',
|
||||
'specification' => '',
|
||||
]
|
||||
],
|
||||
'skip conversion when specification is missing' => [
|
||||
[
|
||||
'name' => 'test-site',
|
||||
],
|
||||
[
|
||||
'name' => 'test-site',
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('sitesCreateProvider')]
|
||||
public function testSitesCreate(array $content, array $expected): void
|
||||
{
|
||||
$model = 'sites.create';
|
||||
|
||||
$result = $this->filter->parse($content, $model);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public static function functionsUpdateProvider(): array
|
||||
{
|
||||
return [
|
||||
'convert specification to buildSpecification and runtimeSpecification' => [
|
||||
[
|
||||
'name' => 'updated-function',
|
||||
'specification' => 's-2vcpu-1gb',
|
||||
],
|
||||
[
|
||||
'name' => 'updated-function',
|
||||
'buildSpecification' => 's-2vcpu-1gb',
|
||||
'runtimeSpecification' => 's-2vcpu-1gb',
|
||||
]
|
||||
],
|
||||
'skip conversion when specification is missing' => [
|
||||
[
|
||||
'name' => 'updated-function',
|
||||
],
|
||||
[
|
||||
'name' => 'updated-function',
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('functionsUpdateProvider')]
|
||||
public function testFunctionsUpdate(array $content, array $expected): void
|
||||
{
|
||||
$model = 'functions.update';
|
||||
|
||||
$result = $this->filter->parse($content, $model);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public static function sitesUpdateProvider(): array
|
||||
{
|
||||
return [
|
||||
'convert specification to buildSpecification and runtimeSpecification' => [
|
||||
[
|
||||
'name' => 'updated-site',
|
||||
'specification' => 's-2vcpu-1gb',
|
||||
],
|
||||
[
|
||||
'name' => 'updated-site',
|
||||
'buildSpecification' => 's-2vcpu-1gb',
|
||||
'runtimeSpecification' => 's-2vcpu-1gb',
|
||||
]
|
||||
],
|
||||
'skip conversion when specification is missing' => [
|
||||
[
|
||||
'name' => 'updated-site',
|
||||
],
|
||||
[
|
||||
'name' => 'updated-site',
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('sitesUpdateProvider')]
|
||||
public function testSitesUpdate(array $content, array $expected): void
|
||||
{
|
||||
$model = 'sites.update';
|
||||
|
||||
$result = $this->filter->parse($content, $model);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public static function unmatchedModelProvider(): array
|
||||
{
|
||||
return [
|
||||
'unmatched model passes through unchanged' => [
|
||||
'databases.create',
|
||||
[
|
||||
'name' => 'test-database',
|
||||
'databaseId' => 'db123',
|
||||
],
|
||||
[
|
||||
'name' => 'test-database',
|
||||
'databaseId' => 'db123',
|
||||
]
|
||||
],
|
||||
'empty content for unmatched model' => [
|
||||
'users.list',
|
||||
[],
|
||||
[]
|
||||
],
|
||||
'content with specification for unmatched model is not converted' => [
|
||||
'deployments.create',
|
||||
[
|
||||
'specification' => 's-1vcpu-512mb',
|
||||
'name' => 'test',
|
||||
],
|
||||
[
|
||||
'specification' => 's-1vcpu-512mb',
|
||||
'name' => 'test',
|
||||
]
|
||||
],
|
||||
'content with version for unmatched model is not converted' => [
|
||||
'databases.createDocument',
|
||||
[
|
||||
'version' => '1.0.0',
|
||||
'data' => 'test',
|
||||
],
|
||||
[
|
||||
'version' => '1.0.0',
|
||||
'data' => 'test',
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('unmatchedModelProvider')]
|
||||
public function testUnmatchedModel(string $model, array $content, array $expected): void
|
||||
{
|
||||
$result = $this->filter->parse($content, $model);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Utopia\Response\Filters;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Filters\V21;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class V21Test extends TestCase
|
||||
{
|
||||
protected ?V21 $filter = null;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->filter = new V21();
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
}
|
||||
|
||||
public static function functionProvider(): array
|
||||
{
|
||||
return [
|
||||
'merge buildSpecification and runtimeSpecification into specification' => [
|
||||
[
|
||||
'name' => 'test-function',
|
||||
'buildSpecification' => 's-1vcpu-512mb',
|
||||
'runtimeSpecification' => 's-1vcpu-256mb',
|
||||
'runtime' => 'node-18.0',
|
||||
],
|
||||
[
|
||||
'name' => 'test-function',
|
||||
'specification' => 's-1vcpu-512mb',
|
||||
'runtime' => 'node-18.0',
|
||||
]
|
||||
],
|
||||
'use buildSpecification when present' => [
|
||||
[
|
||||
'buildSpecification' => 's-2vcpu-1gb',
|
||||
'runtimeSpecification' => 's-1vcpu-512mb',
|
||||
],
|
||||
[
|
||||
'specification' => 's-2vcpu-1gb',
|
||||
]
|
||||
],
|
||||
'fallback to specification when buildSpecification is missing' => [
|
||||
[
|
||||
'specification' => 's-1vcpu-512mb',
|
||||
'runtimeSpecification' => 's-1vcpu-256mb',
|
||||
],
|
||||
[
|
||||
'specification' => 's-1vcpu-512mb',
|
||||
]
|
||||
],
|
||||
'handle missing both buildSpecification and specification' => [
|
||||
[
|
||||
'name' => 'test-function',
|
||||
'runtimeSpecification' => 's-1vcpu-256mb',
|
||||
],
|
||||
[
|
||||
'name' => 'test-function',
|
||||
'specification' => null,
|
||||
]
|
||||
],
|
||||
'handle no spec fields at all' => [
|
||||
[
|
||||
'name' => 'test-function',
|
||||
'runtime' => 'node-18.0',
|
||||
],
|
||||
[
|
||||
'name' => 'test-function',
|
||||
'specification' => null,
|
||||
'runtime' => 'node-18.0',
|
||||
]
|
||||
],
|
||||
'handle empty buildSpecification string' => [
|
||||
[
|
||||
'buildSpecification' => '',
|
||||
'runtimeSpecification' => 's-1vcpu-256mb',
|
||||
],
|
||||
[
|
||||
'specification' => '',
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('functionProvider')]
|
||||
public function testFunction(array $content, array $expected): void
|
||||
{
|
||||
$model = Response::MODEL_FUNCTION;
|
||||
|
||||
$result = $this->filter->parse($content, $model);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public static function functionListProvider(): array
|
||||
{
|
||||
return [
|
||||
'convert list of functions' => [
|
||||
[
|
||||
'total' => 2,
|
||||
'functions' => [
|
||||
[
|
||||
'name' => 'function-1',
|
||||
'buildSpecification' => 's-1vcpu-512mb',
|
||||
'runtimeSpecification' => 's-1vcpu-256mb',
|
||||
],
|
||||
[
|
||||
'name' => 'function-2',
|
||||
'buildSpecification' => 's-2vcpu-1gb',
|
||||
'runtimeSpecification' => 's-1vcpu-512mb',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'total' => 2,
|
||||
'functions' => [
|
||||
[
|
||||
'name' => 'function-1',
|
||||
'specification' => 's-1vcpu-512mb',
|
||||
],
|
||||
[
|
||||
'name' => 'function-2',
|
||||
'specification' => 's-2vcpu-1gb',
|
||||
],
|
||||
],
|
||||
]
|
||||
],
|
||||
'handle empty function list' => [
|
||||
[
|
||||
'total' => 0,
|
||||
'functions' => [],
|
||||
],
|
||||
[
|
||||
'total' => 0,
|
||||
'functions' => [],
|
||||
]
|
||||
],
|
||||
'handle single function in list' => [
|
||||
[
|
||||
'total' => 1,
|
||||
'functions' => [
|
||||
[
|
||||
'buildSpecification' => 's-1vcpu-512mb',
|
||||
'runtimeSpecification' => 's-1vcpu-256mb',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'total' => 1,
|
||||
'functions' => [
|
||||
[
|
||||
'specification' => 's-1vcpu-512mb',
|
||||
],
|
||||
],
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('functionListProvider')]
|
||||
public function testFunctionList(array $content, array $expected): void
|
||||
{
|
||||
$model = Response::MODEL_FUNCTION_LIST;
|
||||
|
||||
$result = $this->filter->parse($content, $model);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public static function siteProvider(): array
|
||||
{
|
||||
return [
|
||||
'merge buildSpecification and runtimeSpecification into specification' => [
|
||||
[
|
||||
'name' => 'test-site',
|
||||
'buildSpecification' => 's-1vcpu-512mb',
|
||||
'runtimeSpecification' => 's-1vcpu-256mb',
|
||||
'framework' => 'nextjs',
|
||||
],
|
||||
[
|
||||
'name' => 'test-site',
|
||||
'specification' => 's-1vcpu-512mb',
|
||||
'framework' => 'nextjs',
|
||||
]
|
||||
],
|
||||
'use buildSpecification when present' => [
|
||||
[
|
||||
'buildSpecification' => 's-2vcpu-1gb',
|
||||
'runtimeSpecification' => 's-1vcpu-512mb',
|
||||
],
|
||||
[
|
||||
'specification' => 's-2vcpu-1gb',
|
||||
]
|
||||
],
|
||||
'fallback to specification when buildSpecification is missing' => [
|
||||
[
|
||||
'specification' => 's-1vcpu-512mb',
|
||||
'runtimeSpecification' => 's-1vcpu-256mb',
|
||||
],
|
||||
[
|
||||
'specification' => 's-1vcpu-512mb',
|
||||
]
|
||||
],
|
||||
'handle missing both buildSpecification and specification' => [
|
||||
[
|
||||
'name' => 'test-site',
|
||||
'runtimeSpecification' => 's-1vcpu-256mb',
|
||||
],
|
||||
[
|
||||
'name' => 'test-site',
|
||||
'specification' => null,
|
||||
]
|
||||
],
|
||||
'handle no spec fields at all' => [
|
||||
[
|
||||
'name' => 'test-site',
|
||||
'framework' => 'react',
|
||||
],
|
||||
[
|
||||
'name' => 'test-site',
|
||||
'specification' => null,
|
||||
'framework' => 'react',
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('siteProvider')]
|
||||
public function testSite(array $content, array $expected): void
|
||||
{
|
||||
$model = Response::MODEL_SITE;
|
||||
|
||||
$result = $this->filter->parse($content, $model);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public static function siteListProvider(): array
|
||||
{
|
||||
return [
|
||||
'convert list of sites' => [
|
||||
[
|
||||
'total' => 2,
|
||||
'sites' => [
|
||||
[
|
||||
'name' => 'site-1',
|
||||
'buildSpecification' => 's-1vcpu-512mb',
|
||||
'runtimeSpecification' => 's-1vcpu-256mb',
|
||||
],
|
||||
[
|
||||
'name' => 'site-2',
|
||||
'buildSpecification' => 's-2vcpu-1gb',
|
||||
'runtimeSpecification' => 's-1vcpu-512mb',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'total' => 2,
|
||||
'sites' => [
|
||||
[
|
||||
'name' => 'site-1',
|
||||
'specification' => 's-1vcpu-512mb',
|
||||
],
|
||||
[
|
||||
'name' => 'site-2',
|
||||
'specification' => 's-2vcpu-1gb',
|
||||
],
|
||||
],
|
||||
]
|
||||
],
|
||||
'handle empty site list' => [
|
||||
[
|
||||
'total' => 0,
|
||||
'sites' => [],
|
||||
],
|
||||
[
|
||||
'total' => 0,
|
||||
'sites' => [],
|
||||
]
|
||||
],
|
||||
'handle single site in list' => [
|
||||
[
|
||||
'total' => 1,
|
||||
'sites' => [
|
||||
[
|
||||
'name' => 'my-site',
|
||||
'buildSpecification' => 's-1vcpu-512mb',
|
||||
'runtimeSpecification' => 's-1vcpu-256mb',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'total' => 1,
|
||||
'sites' => [
|
||||
[
|
||||
'name' => 'my-site',
|
||||
'specification' => 's-1vcpu-512mb',
|
||||
],
|
||||
],
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('siteListProvider')]
|
||||
public function testSiteList(array $content, array $expected): void
|
||||
{
|
||||
$model = Response::MODEL_SITE_LIST;
|
||||
|
||||
$result = $this->filter->parse($content, $model);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public static function documentProvider(): array
|
||||
{
|
||||
return [
|
||||
'cast $sequence to int' => [
|
||||
[
|
||||
'$id' => 'doc1',
|
||||
'$sequence' => '123',
|
||||
'name' => 'test',
|
||||
],
|
||||
[
|
||||
'$id' => 'doc1',
|
||||
'$sequence' => 123,
|
||||
'name' => 'test',
|
||||
]
|
||||
],
|
||||
'non-numeric $sequence becomes 0' => [
|
||||
[
|
||||
'$id' => 'doc1',
|
||||
'$sequence' => 'abc',
|
||||
],
|
||||
[
|
||||
'$id' => 'doc1',
|
||||
'$sequence' => 0,
|
||||
]
|
||||
],
|
||||
'nested relationship document' => [
|
||||
[
|
||||
'$id' => 'doc1',
|
||||
'$sequence' => '1',
|
||||
'author' => [
|
||||
'$id' => 'doc2',
|
||||
'$sequence' => '2',
|
||||
'name' => 'John',
|
||||
],
|
||||
],
|
||||
[
|
||||
'$id' => 'doc1',
|
||||
'$sequence' => 1,
|
||||
'author' => [
|
||||
'$id' => 'doc2',
|
||||
'$sequence' => 2,
|
||||
'name' => 'John',
|
||||
],
|
||||
]
|
||||
],
|
||||
'nested array of relationship documents' => [
|
||||
[
|
||||
'$id' => 'doc1',
|
||||
'$sequence' => '1',
|
||||
'comments' => [
|
||||
[
|
||||
'$id' => 'doc2',
|
||||
'$sequence' => '2',
|
||||
'text' => 'hello',
|
||||
],
|
||||
[
|
||||
'$id' => 'doc3',
|
||||
'$sequence' => '3',
|
||||
'text' => 'world',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'$id' => 'doc1',
|
||||
'$sequence' => 1,
|
||||
'comments' => [
|
||||
[
|
||||
'$id' => 'doc2',
|
||||
'$sequence' => 2,
|
||||
'text' => 'hello',
|
||||
],
|
||||
[
|
||||
'$id' => 'doc3',
|
||||
'$sequence' => 3,
|
||||
'text' => 'world',
|
||||
],
|
||||
],
|
||||
]
|
||||
],
|
||||
'deeply nested relationships' => [
|
||||
[
|
||||
'$id' => 'doc1',
|
||||
'$sequence' => '1',
|
||||
'author' => [
|
||||
'$id' => 'doc2',
|
||||
'$sequence' => '2',
|
||||
'profile' => [
|
||||
'$id' => 'doc3',
|
||||
'$sequence' => '3',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'$id' => 'doc1',
|
||||
'$sequence' => 1,
|
||||
'author' => [
|
||||
'$id' => 'doc2',
|
||||
'$sequence' => 2,
|
||||
'profile' => [
|
||||
'$id' => 'doc3',
|
||||
'$sequence' => 3,
|
||||
],
|
||||
],
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('documentProvider')]
|
||||
public function testDocument(array $content, array $expected): void
|
||||
{
|
||||
$result = $this->filter->parse($content, Response::MODEL_DOCUMENT);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
#[DataProvider('documentProvider')]
|
||||
public function testRow(array $content, array $expected): void
|
||||
{
|
||||
$result = $this->filter->parse($content, Response::MODEL_ROW);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public static function defaultPassthroughProvider(): array
|
||||
{
|
||||
return [
|
||||
'unmatched model passes through unchanged' => [
|
||||
Response::MODEL_DOCUMENT,
|
||||
[
|
||||
'name' => 'test-doc',
|
||||
'$id' => 'doc123',
|
||||
'data' => 'some-value',
|
||||
],
|
||||
[
|
||||
'name' => 'test-doc',
|
||||
'$id' => 'doc123',
|
||||
'data' => 'some-value',
|
||||
]
|
||||
],
|
||||
'empty content passes through unchanged' => [
|
||||
Response::MODEL_DOCUMENT,
|
||||
[],
|
||||
[]
|
||||
],
|
||||
'deployment model passes through unchanged' => [
|
||||
Response::MODEL_DEPLOYMENT,
|
||||
[
|
||||
'id' => 'deployment123',
|
||||
'buildSpecification' => 's-1vcpu-512mb',
|
||||
'runtimeSpecification' => 's-1vcpu-256mb',
|
||||
],
|
||||
[
|
||||
'id' => 'deployment123',
|
||||
'buildSpecification' => 's-1vcpu-512mb',
|
||||
'runtimeSpecification' => 's-1vcpu-256mb',
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('defaultPassthroughProvider')]
|
||||
public function testDefaultPassthrough(string $model, array $content, array $expected): void
|
||||
{
|
||||
$result = $this->filter->parse($content, $model);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user