mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
124
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3df219df66 | ||
|
|
0ce3978f2c | ||
|
|
d6be6e165b | ||
|
|
7839b3bb3f | ||
|
|
5320c9441b | ||
|
|
8ad73aa6bb | ||
|
|
5cbbacaae5 | ||
|
|
5ee92e1844 | ||
|
|
a546c41662 | ||
|
|
df16c84b51 | ||
|
|
6bde54675e | ||
|
|
90ef2d7487 | ||
|
|
725ba86239 | ||
|
|
99641f24a5 | ||
|
|
34935b8eed | ||
|
|
08ad7d7f71 | ||
|
|
71300383b2 | ||
|
|
8785aa9877 | ||
|
|
62b7d5558f | ||
|
|
fbbab0f7e1 | ||
|
|
86b9599a57 | ||
|
|
526b390c15 | ||
|
|
b73ba68bfb | ||
|
|
d98bd8c972 | ||
|
|
81c580bf50 | ||
|
|
c0bba74eee | ||
|
|
337d47b1d9 | ||
|
|
8ffe48d948 | ||
|
|
4050b9ded1 | ||
|
|
d099167d18 | ||
|
|
9d3255f5cd | ||
|
|
f03cc847f8 | ||
|
|
6088fd55c8 | ||
|
|
1aeee8c407 | ||
|
|
18b9769672 | ||
|
|
8eed06678b | ||
|
|
7a9a2899ff | ||
|
|
36486ccc93 | ||
|
|
794d8eac5b | ||
|
|
32ebfc6cb8 | ||
|
|
e1b8f5bf98 | ||
|
|
4d86e67006 | ||
|
|
b3e3b2a330 | ||
|
|
e010bf25d5 | ||
|
|
aaf91f3816 | ||
|
|
bae61e8a05 | ||
|
|
dfbf45f4cc | ||
|
|
c264db6146 | ||
|
|
cccafeff0c | ||
|
|
360d08f087 | ||
|
|
d8e97ae47d | ||
|
|
d13e6d75f0 | ||
|
|
4447396859 | ||
|
|
4d1f229ec7 | ||
|
|
4f733f5984 | ||
|
|
701f557755 | ||
|
|
8ab26aab44 | ||
|
|
ec3aa2b54f | ||
|
|
2d636ff7ec | ||
|
|
fd42b8fa64 | ||
|
|
9d7df34590 | ||
|
|
e75fc5b859 | ||
|
|
57b8305144 | ||
|
|
a429fb5860 | ||
|
|
aca11ed073 | ||
|
|
86123c9e93 | ||
|
|
a58ea1123b | ||
|
|
dae9cbcf45 | ||
|
|
05f2d2b9cf | ||
|
|
c1f61b22aa | ||
|
|
980762fc3e | ||
|
|
c96836b1c0 | ||
|
|
15917ac7ba | ||
|
|
cd851bff24 | ||
|
|
3f5dcc81fd | ||
|
|
f5a732d231 | ||
|
|
72dfd8a7bc | ||
|
|
11f80fc2ed | ||
|
|
547709a1d8 | ||
|
|
ccb0ddd578 | ||
|
|
b2ce95a0cd | ||
|
|
ed9b47f6ce | ||
|
|
2a357511ea | ||
|
|
67d24d3ef1 | ||
|
|
87ed7c3817 | ||
|
|
8f176166c9 | ||
|
|
a0ef145b92 | ||
|
|
9e1f8af103 | ||
|
|
b055ff1066 | ||
|
|
f0cbfbbbe4 | ||
|
|
bda823ac0e | ||
|
|
3dd5a51ba4 | ||
|
|
5afc8f462d | ||
|
|
ed0c7b4e12 | ||
|
|
a3f6cf4645 | ||
|
|
9a175c5098 | ||
|
|
5465be6301 | ||
|
|
46a457bfa3 | ||
|
|
4c989f99c3 | ||
|
|
8f1d73a6cb | ||
|
|
01b5fa8ecb | ||
|
|
d73b7a70d8 | ||
|
|
f71a2dfddc | ||
|
|
2f2da98cca | ||
|
|
54997638e8 | ||
|
|
70b9c60e2c | ||
|
|
49d2db65e6 | ||
|
|
1fdcca9592 | ||
|
|
cb8640b56f | ||
|
|
3f12062259 | ||
|
|
9553f8a9f8 | ||
|
|
ca105ff9bc | ||
|
|
7e3114d733 | ||
|
|
ef4b9c4934 | ||
|
|
1928605bd9 | ||
|
|
340ce9d56b | ||
|
|
e6d5c216eb | ||
|
|
d25ccb784d | ||
|
|
78715e4a1a | ||
|
|
df57ee2a32 | ||
|
|
6d4a66fbb3 | ||
|
|
3aee54747c | ||
|
|
d2423a5bb5 | ||
|
|
c0c053ff20 |
@@ -0,0 +1,174 @@
|
||||
# Parallel Chunk Upload Support for utopia-php/storage
|
||||
|
||||
## Context
|
||||
|
||||
The Appwrite API now supports out-of-order chunked uploads (chunks can arrive in any sequence). The next step is **parallel uploads** — multiple chunks uploaded simultaneously via separate HTTP requests. The SDK guarantees the first chunk is sent before any parallel chunks, so the document creation race is handled at the API layer. However, the storage device layer has a race condition that must be fixed.
|
||||
|
||||
## Problem: `Local::joinChunks()` Race
|
||||
|
||||
When two requests upload the final missing chunks in parallel, both can observe `countChunks() == $chunks` and call `joinChunks()` simultaneously.
|
||||
|
||||
### Current behavior (loser throws)
|
||||
|
||||
```php
|
||||
// Local::joinChunks()
|
||||
$dest = \fopen($tmpAssemble, 'wb');
|
||||
// ... stream all parts into $tmpAssemble ...
|
||||
|
||||
if (! \rename($tmpAssemble, $path)) {
|
||||
\unlink($tmpAssemble);
|
||||
throw new Exception('Failed to finalize assembled file '.$path);
|
||||
}
|
||||
```
|
||||
|
||||
The winner succeeds with `rename()`. The loser gets `false` from `rename()` (file already exists at `$path`) and throws a 500-error exception. The client that lost the race receives an error even though the file is fully assembled.
|
||||
|
||||
### Required behavior
|
||||
|
||||
If `$path` already exists, another request already assembled the file. The loser should **silently succeed** — the file is complete, nothing more to do.
|
||||
|
||||
## Proposed Changes
|
||||
|
||||
### 1. `Local::joinChunks()` — Handle assembly race
|
||||
|
||||
Before opening `$tmpAssemble`, check if the final file already exists. If it does, skip assembly entirely.
|
||||
|
||||
```php
|
||||
private function joinChunks(string $path, int $chunks): void
|
||||
{
|
||||
// Race winner already assembled the file
|
||||
if (\file_exists($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tmp = \dirname($path).DIRECTORY_SEPARATOR.'tmp_'.asename($path);
|
||||
$tmpAssemble = \dirname($path).DIRECTORY_SEPARATOR.'tmp_assemble_'.asename($path);
|
||||
|
||||
// ... rest of assembly logic ...
|
||||
|
||||
if (! \rename($tmpAssemble, $path)) {
|
||||
// Another request may have won the race between fclose and rename
|
||||
if (\file_exists($path)) {
|
||||
\unlink($tmpAssemble);
|
||||
return;
|
||||
}
|
||||
\unlink($tmpAssemble);
|
||||
throw new Exception('Failed to finalize assembled file '.$path);
|
||||
}
|
||||
|
||||
// ... cleanup ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. `Local::countChunks()` — Reliability under concurrent writes
|
||||
|
||||
`countChunks()` uses `glob()` on the temp directory. Under heavy parallel load, `glob()` might miss files or return inconsistent counts. The current implementation is already fairly robust (it validates `.part.\d+` suffix), but we should document that the return value is a best-effort snapshot.
|
||||
|
||||
No code change needed here unless tests reveal issues.
|
||||
|
||||
### 3. Tests — Concurrent chunk uploads
|
||||
|
||||
Add a test that simulates two parallel requests completing a multi-chunk upload:
|
||||
|
||||
```php
|
||||
public function testParallelChunkUpload(): void
|
||||
{
|
||||
$storage = $this->makeJoinTestStorage();
|
||||
$dest = $storage->getRoot().DIRECTORY_SEPARATOR.'parallel.dat';
|
||||
|
||||
// Upload chunk 1 (creates temp directory)
|
||||
$storage->uploadData('AAAA', $dest, 'application/octet-stream', 1, 2);
|
||||
|
||||
// Simulate two parallel requests uploading the last chunk
|
||||
// In a real test, use pcntl_fork() or pthreads for true concurrency
|
||||
// For the test suite, sequential calls are sufficient if we verify
|
||||
// the second call doesn't throw after the first completed assembly
|
||||
$storage->uploadData('BBBB', $dest, 'application/octet-stream', 2, 2);
|
||||
|
||||
// Verify file exists and is correct
|
||||
$this->assertTrue(\file_exists($dest));
|
||||
$this->assertSame('AAAABBBB', \file_get_contents($dest));
|
||||
|
||||
// Verify second assembly attempt doesn't throw
|
||||
// (This simulates the race where another request already assembled)
|
||||
try {
|
||||
$storage->uploadData('BBBB', $dest, 'application/octet-stream', 2, 2);
|
||||
} catch (\Exception $e) {
|
||||
$this->fail('Duplicate assembly should not throw: '.$e->getMessage());
|
||||
}
|
||||
|
||||
$storage->delete($storage->getRoot(), true);
|
||||
}
|
||||
```
|
||||
|
||||
A more realistic concurrent test using `pcntl_fork()`:
|
||||
|
||||
```php
|
||||
public function testParallelChunkUploadWithFork(): void
|
||||
{
|
||||
if (!\function_exists('pcntl_fork')) {
|
||||
$this->markTestSkipped('pcntl extension required for fork-based concurrency test');
|
||||
}
|
||||
|
||||
$storage = $this->makeJoinTestStorage();
|
||||
$dest = $storage->getRoot().DIRECTORY_SEPARATOR.'parallel-fork.dat';
|
||||
|
||||
// Pre-upload chunk 1
|
||||
$storage->uploadData('AAAA', $dest, 'application/octet-stream', 1, 2);
|
||||
|
||||
$pid = pcntl_fork();
|
||||
if ($pid === -1) {
|
||||
$this->fail('Failed to fork');
|
||||
} elseif ($pid === 0) {
|
||||
// Child process: upload chunk 2
|
||||
try {
|
||||
$storage->uploadData('BBBB', $dest, 'application/octet-stream', 2, 2);
|
||||
exit(0);
|
||||
} catch (\Exception $e) {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Parent process: also upload chunk 2 (race condition)
|
||||
$parentSuccess = true;
|
||||
try {
|
||||
$storage->uploadData('BBBB', $dest, 'application/octet-stream', 2, 2);
|
||||
} catch (\Exception $e) {
|
||||
$parentSuccess = false;
|
||||
}
|
||||
|
||||
pcntl_waitpid($pid, $status);
|
||||
$childSuccess = pcntl_wexitstatus($status) === 0;
|
||||
|
||||
// At least one should succeed
|
||||
$this->assertTrue($parentSuccess || $childSuccess, 'At least one parallel upload should succeed');
|
||||
|
||||
// File should be correctly assembled
|
||||
$this->assertTrue(\file_exists($dest));
|
||||
$this->assertSame('AAAABBBB', \file_get_contents($dest));
|
||||
|
||||
$storage->delete($storage->getRoot(), true);
|
||||
}
|
||||
```
|
||||
|
||||
## S3 Device
|
||||
|
||||
S3 already handles out-of-order multipart uploads natively. The `completeMultipartUpload` call with `ksort()` sorts parts by number regardless of upload order. However, parallel `completeMultipartUpload` calls for the same `uploadId` would still be problematic.
|
||||
|
||||
This is an **API-layer concern** — the Appwrite API should ensure only one request calls `completeMultipartUpload` per upload. The S3 device itself does not need changes.
|
||||
|
||||
## Files to Change
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/Storage/Device/Local.php` | Add `file_exists($path)` guard at start of `joinChunks()` and in `rename()` failure handler |
|
||||
| `tests/Storage/Device/LocalTest.php` | Add `testParallelChunkUpload` and `testParallelChunkUploadWithFork` |
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
Fully backwards compatible. The change only affects the error path when `rename()` fails due to an existing file. Previously it threw; now it returns silently. No public API signatures change.
|
||||
|
||||
## Related PRs
|
||||
|
||||
- Appwrite server PR: https://github.com/appwrite/appwrite/pull/12138 (out-of-order upload support)
|
||||
- This storage PR is a prerequisite for the follow-up Appwrite PR that enables parallel chunk uploads at the API level.
|
||||
@@ -47,6 +47,8 @@ _APP_DB_SCHEMA=appwrite
|
||||
_APP_DB_USER=user
|
||||
_APP_DB_PASS=password
|
||||
_APP_DB_ROOT_PASS=rootsecretpassword
|
||||
_APP_DATABASE_SHARED_TABLES=
|
||||
_APP_DATABASE_SHARED_NAMESPACE=
|
||||
_APP_DB_ADAPTER_DOCUMENTSDB=mongodb
|
||||
_APP_DB_HOST_DOCUMENTSDB=mongodb
|
||||
_APP_DB_PORT_DOCUMENTSDB=27017
|
||||
|
||||
+93
-57
@@ -7,6 +7,7 @@ concurrency:
|
||||
env:
|
||||
COMPOSE_FILE: docker-compose.yml
|
||||
IMAGE: appwrite-dev
|
||||
REGISTRY_IMAGE: ghcr.io/${{ github.repository }}/appwrite-dev
|
||||
K6_VERSION: '0.53.0'
|
||||
|
||||
on:
|
||||
@@ -19,6 +20,10 @@ on:
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
dependencies:
|
||||
name: Checks / Dependencies
|
||||
@@ -258,32 +263,30 @@ jobs:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build Appwrite
|
||||
- name: Build and push Appwrite
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: ${{ env.IMAGE }}
|
||||
load: true
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
outputs: type=docker,dest=/tmp/${{ env.IMAGE }}.tar
|
||||
target: development
|
||||
build-args: |
|
||||
DEBUG=false
|
||||
TESTING=true
|
||||
VERSION=dev
|
||||
|
||||
- name: Upload Docker Image
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
retention-days: 1
|
||||
|
||||
unit:
|
||||
name: Tests / Unit
|
||||
runs-on: ubuntu-latest
|
||||
@@ -291,26 +294,32 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
packages: read
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull Docker Image
|
||||
run: |
|
||||
docker pull ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
|
||||
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 5
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
docker compose up -d --quiet-pull --wait
|
||||
|
||||
@@ -338,26 +347,32 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
packages: read
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull Docker Image
|
||||
run: |
|
||||
docker pull ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
|
||||
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 5
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
docker compose up -d --quiet-pull --wait
|
||||
|
||||
@@ -396,6 +411,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
packages: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -450,16 +466,10 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Set environment
|
||||
run: |
|
||||
echo "_APP_OPTIONS_ROUTER_PROTECTION=enabled" >> $GITHUB_ENV
|
||||
|
||||
|
||||
if [ "${{ matrix.database }}" = "MariaDB" ]; then
|
||||
echo "COMPOSE_PROFILES=mariadb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_ADAPTER=mariadb" >> $GITHUB_ENV
|
||||
@@ -483,6 +493,18 @@ jobs:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull Docker Image
|
||||
run: |
|
||||
docker pull ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
|
||||
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
@@ -491,7 +513,6 @@ jobs:
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
docker compose up -d --quiet-pull --wait
|
||||
|
||||
@@ -545,6 +566,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
packages: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -555,18 +577,24 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull Docker Image
|
||||
run: |
|
||||
docker pull ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
|
||||
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
@@ -575,7 +603,6 @@ jobs:
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
docker compose up -d --quiet-pull --wait
|
||||
|
||||
@@ -606,6 +633,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
packages: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -614,18 +642,24 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull Docker Image
|
||||
run: |
|
||||
docker pull ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
|
||||
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
@@ -633,7 +667,6 @@ jobs:
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
docker compose up -d --quiet-pull --wait
|
||||
|
||||
@@ -675,28 +708,31 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
packages: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Load Appwrite image
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull Appwrite image
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker tag ${{ env.IMAGE }} ${{ env.IMAGE }}:after
|
||||
docker pull ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
|
||||
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}
|
||||
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}:after
|
||||
|
||||
- name: Setup k6
|
||||
uses: grafana/setup-k6-action@ffe7d7290dfa715e48c2ccc924d068444c94bde2
|
||||
|
||||
@@ -5,6 +5,11 @@ on:
|
||||
types:
|
||||
- closed
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -36,4 +41,29 @@ jobs:
|
||||
done
|
||||
done
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Cleanup GHCR image
|
||||
continue-on-error: true
|
||||
run: |
|
||||
package_path="${GITHUB_REPOSITORY#*/}/appwrite-dev"
|
||||
encoded_path="$(printf '%s' "$package_path" | jq -Rr @uri)"
|
||||
|
||||
gh api --paginate "/repos/${GITHUB_REPOSITORY}/pulls/${{ github.event.pull_request.number }}/commits" --jq '.[].sha' | while read -r sha; do
|
||||
version_ids=$(gh api --paginate -H "Accept: application/vnd.github+json" \
|
||||
"/orgs/${GITHUB_REPOSITORY_OWNER}/packages/container/${encoded_path}/versions" \
|
||||
--jq ".[] | select(.metadata.container.tags | index(\"${sha}\")) | .id")
|
||||
|
||||
if [ -z "$version_ids" ]; then
|
||||
echo "No GHCR version found for SHA ${sha}"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "$version_ids" | while read -r version_id; do
|
||||
gh api --method DELETE -H "Accept: application/vnd.github+json" \
|
||||
"/orgs/${GITHUB_REPOSITORY_OWNER}/packages/container/${encoded_path}/versions/${version_id}"
|
||||
echo "Deleted ${package_path}:${sha} (version ${version_id})"
|
||||
done
|
||||
done
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -24,9 +24,11 @@ jobs:
|
||||
ignore-unfixed: 'false'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
- name: Upload Docker Image Scan Results
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
if: always() && hashFiles('trivy-image-results.sarif') != ''
|
||||
with:
|
||||
sarif_file: 'trivy-image-results.sarif'
|
||||
category: 'trivy-image'
|
||||
|
||||
scan-code:
|
||||
name: Scan Code
|
||||
@@ -42,6 +44,8 @@ jobs:
|
||||
output: 'trivy-fs-results.sarif'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
- name: Upload Code Scan Results
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
if: always() && hashFiles('trivy-fs-results.sarif') != ''
|
||||
with:
|
||||
sarif_file: 'trivy-fs-results.sarif'
|
||||
category: 'trivy-source'
|
||||
|
||||
@@ -28,4 +28,3 @@ docker-compose.web-installer.yml
|
||||
.env.web-installer
|
||||
docker-compose.web-installer.yml.**.backup
|
||||
tests/playwright/screenshots
|
||||
.claude/scheduled_tasks.lock
|
||||
|
||||
+8
-2
@@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
|
||||
--no-plugins --no-scripts --prefer-dist \
|
||||
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
|
||||
|
||||
FROM appwrite/base:1.0.1 AS base
|
||||
FROM appwrite/base:1.2.1 AS base
|
||||
|
||||
LABEL maintainer="team@appwrite.io"
|
||||
|
||||
@@ -24,6 +24,10 @@ ENV _APP_VERSION=$VERSION \
|
||||
_APP_HOME=https://appwrite.io
|
||||
|
||||
RUN \
|
||||
if [ "$DEBUG" != "true" ]; then \
|
||||
rm -f /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini && \
|
||||
rm -f /usr/local/lib/php/extensions/no-debug-non-zts-*/xdebug.so; \
|
||||
fi && \
|
||||
if [ "$DEBUG" == "true" ]; then \
|
||||
apk add boost boost-dev; \
|
||||
fi
|
||||
@@ -89,6 +93,7 @@ RUN chmod +x /usr/local/bin/doctor && \
|
||||
chmod +x /usr/local/bin/worker-functions && \
|
||||
chmod +x /usr/local/bin/worker-mails && \
|
||||
chmod +x /usr/local/bin/worker-messaging && \
|
||||
chmod +x /usr/local/bin/worker-notifications && \
|
||||
chmod +x /usr/local/bin/worker-migrations && \
|
||||
chmod +x /usr/local/bin/worker-webhooks && \
|
||||
chmod +x /usr/local/bin/worker-stats-usage && \
|
||||
@@ -100,7 +105,8 @@ RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/
|
||||
FROM base AS production
|
||||
|
||||
RUN rm -rf /usr/src/code/app/config/specs && \
|
||||
rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20240924/xdebug.so && \
|
||||
rm -f /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini && \
|
||||
rm -f /usr/local/lib/php/extensions/no-debug-non-zts-*/xdebug.so && \
|
||||
find /usr -name '*.a' -delete 2>/dev/null || true && \
|
||||
find /usr -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true && \
|
||||
find /usr -name '*.pyc' -delete 2>/dev/null || true
|
||||
|
||||
+20
@@ -157,12 +157,19 @@ $container->set('getProjectDB', function (Group $pools, Database $dbForPlatform,
|
||||
}
|
||||
|
||||
if (isset($databases[$dsn->getHost()])) {
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$projectCollections = $collections['projects'] ?? [];
|
||||
$projectsGlobalCollections = array_keys($projectCollections);
|
||||
$projectsGlobalCollections[] = 'audit';
|
||||
|
||||
$database = $databases[$dsn->getHost()];
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
@@ -182,9 +189,16 @@ $container->set('getProjectDB', function (Group $pools, Database $dbForPlatform,
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$projectCollections = $collections['projects'] ?? [];
|
||||
$projectsGlobalCollections = array_keys($projectCollections);
|
||||
$projectsGlobalCollections[] = 'audit';
|
||||
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
@@ -212,6 +226,11 @@ $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization
|
||||
return $database;
|
||||
}
|
||||
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$logsCollections = $collections['logs'] ?? [];
|
||||
$logsCollections = array_keys($logsCollections);
|
||||
|
||||
$adapter = new DatabasePool($pools->get('logs'));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
@@ -220,6 +239,7 @@ $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization
|
||||
->setAuthorization($authorization)
|
||||
->setSharedTables(true)
|
||||
->setNamespace('logsV1')
|
||||
->setGlobalCollections($logsCollections)
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_TASK)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
|
||||
|
||||
|
||||
@@ -1011,6 +1011,132 @@ $platformCollections = [
|
||||
],
|
||||
],
|
||||
|
||||
'alerts' => [
|
||||
'$collection' => ID::custom(Database::METADATA),
|
||||
'$id' => ID::custom('alerts'),
|
||||
'name' => 'Alerts',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => ID::custom('messageId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('type'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 64,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => 'info',
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('channel'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 64,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('userId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('teamId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('projectId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('title'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 256,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('body'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16384,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => ID::custom('_key_messageId'),
|
||||
'type' => Database::INDEX_UNIQUE,
|
||||
'attributes' => ['messageId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_userId'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['userId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_teamId'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['teamId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_projectId'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['projectId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'certificates' => [
|
||||
'$collection' => ID::custom(Database::METADATA),
|
||||
'$id' => ID::custom('certificates'),
|
||||
|
||||
@@ -384,7 +384,7 @@ return [
|
||||
],
|
||||
Exception::API_KEY_EXPIRED => [
|
||||
'name' => Exception::API_KEY_EXPIRED,
|
||||
'description' => 'The dynamic API key has expired. Please don\'t use dynamic API keys for more than duration of the execution.',
|
||||
'description' => 'The ephemeral API key has expired. Please don\'t use ephemeral API keys for more than duration of the execution.',
|
||||
'code' => 401,
|
||||
],
|
||||
|
||||
|
||||
@@ -28,6 +28,16 @@
|
||||
"emails.invitation.thanks": "Gracias.,",
|
||||
"emails.invitation.buttonText": "Aceptar invitación a {{team}}",
|
||||
"emails.invitation.signature": "El equipo de {{project}}",
|
||||
"emails.sessionAlert.subject": "Alerta de seguridad: nueva sesión en tu cuenta de {{project}}",
|
||||
"emails.sessionAlert.preview": "Nuevo inicio de sesión detectado en {{project}} a las {{time}} UTC.",
|
||||
"emails.sessionAlert.hello": "Hola {{user}},",
|
||||
"emails.sessionAlert.body": "Se ha creado una nueva sesión en tu cuenta de {{b}}{{project}}{{/b}}, {{b}}el {{date}} de {{year}} a las {{time}} UTC{{/b}}.\nEstos son los detalles de la nueva sesión:",
|
||||
"emails.sessionAlert.listDevice": "Dispositivo: {{b}}{{device}}{{/b}}",
|
||||
"emails.sessionAlert.listIpAddress": "Dirección IP: {{b}}{{ipAddress}}{{/b}}",
|
||||
"emails.sessionAlert.listCountry": "País: {{b}}{{country}}{{/b}}",
|
||||
"emails.sessionAlert.footer": "Si has sido tú, no tienes que hacer nada más.\nSi no has iniciado esta sesión o sospechas actividad no autorizada, protege tu cuenta.",
|
||||
"emails.sessionAlert.thanks": "Gracias,",
|
||||
"emails.sessionAlert.signature": "El equipo de {{project}}",
|
||||
"locale.country.unknown": "Desconocido",
|
||||
"countries.af": "Afganistán",
|
||||
"countries.ao": "Angola",
|
||||
|
||||
@@ -21,8 +21,8 @@ $member = [
|
||||
'projects.read',
|
||||
'locale.read',
|
||||
'avatars.read',
|
||||
'execution.read',
|
||||
'execution.write',
|
||||
'executions.read',
|
||||
'executions.write',
|
||||
'targets.read',
|
||||
'targets.write',
|
||||
'subscribers.write',
|
||||
@@ -81,8 +81,8 @@ $admins = [
|
||||
'sites.write',
|
||||
'log.read',
|
||||
'log.write',
|
||||
'execution.read',
|
||||
'execution.write',
|
||||
'executions.read',
|
||||
'executions.write',
|
||||
'rules.read',
|
||||
'rules.write',
|
||||
'migrations.read',
|
||||
@@ -123,7 +123,7 @@ return [
|
||||
'files.write',
|
||||
'locale.read',
|
||||
'avatars.read',
|
||||
'execution.write',
|
||||
'executions.write',
|
||||
],
|
||||
],
|
||||
User::ROLE_USERS => [
|
||||
|
||||
+281
-180
@@ -1,239 +1,340 @@
|
||||
<?php
|
||||
|
||||
return [ // List of publicly visible scopes
|
||||
'sessions.write' => [
|
||||
'description' => 'Access to create, update, and delete user sessions',
|
||||
],
|
||||
'users.read' => [
|
||||
'description' => 'Access to read your project\'s users',
|
||||
],
|
||||
'users.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s users',
|
||||
],
|
||||
'teams.read' => [
|
||||
'description' => 'Access to read your project\'s teams',
|
||||
],
|
||||
'teams.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s teams',
|
||||
],
|
||||
'databases.read' => [
|
||||
'description' => 'Access to read your project\'s databases',
|
||||
],
|
||||
'databases.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s databases',
|
||||
],
|
||||
'collections.read' => [
|
||||
'description' => 'Access to read your project\'s database collections',
|
||||
],
|
||||
'collections.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s database collections',
|
||||
],
|
||||
'tables.read' => [
|
||||
'description' => 'Access to read your project\'s database tables',
|
||||
],
|
||||
'tables.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s database tables',
|
||||
],
|
||||
'attributes.read' => [
|
||||
'description' => 'Access to read your project\'s database collection\'s attributes',
|
||||
],
|
||||
'attributes.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s database collection\'s attributes',
|
||||
],
|
||||
'columns.read' => [
|
||||
'description' => 'Access to read your project\'s database table\'s columns',
|
||||
],
|
||||
'columns.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s database table\'s columns',
|
||||
],
|
||||
'indexes.read' => [
|
||||
'description' => 'Access to read your project\'s database table\'s indexes',
|
||||
],
|
||||
'indexes.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s database table\'s indexes',
|
||||
],
|
||||
'documents.read' => [
|
||||
'description' => 'Access to read your project\'s database documents',
|
||||
],
|
||||
'documents.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s database documents',
|
||||
],
|
||||
'rows.read' => [
|
||||
'description' => 'Access to read your project\'s database rows',
|
||||
],
|
||||
'rows.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s database rows',
|
||||
],
|
||||
'files.read' => [
|
||||
'description' => 'Access to read your project\'s storage files and preview images',
|
||||
],
|
||||
'files.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s storage files',
|
||||
],
|
||||
'buckets.read' => [
|
||||
'description' => 'Access to read your project\'s storage buckets',
|
||||
],
|
||||
'buckets.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s storage buckets',
|
||||
],
|
||||
'functions.read' => [
|
||||
'description' => 'Access to read your project\'s functions and code deployments',
|
||||
],
|
||||
'functions.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s functions and code deployments',
|
||||
],
|
||||
'sites.read' => [
|
||||
'description' => 'Access to read your project\'s sites and deployments',
|
||||
],
|
||||
'sites.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s sites and deployments',
|
||||
],
|
||||
'log.read' => [
|
||||
'description' => 'Access to read your site\'s logs',
|
||||
],
|
||||
'log.write' => [
|
||||
'description' => 'Access to update, and delete your site\'s logs',
|
||||
],
|
||||
'execution.read' => [
|
||||
'description' => 'Access to read your project\'s execution logs',
|
||||
],
|
||||
'execution.write' => [
|
||||
'description' => 'Access to execute your project\'s functions',
|
||||
],
|
||||
'locale.read' => [
|
||||
'description' => 'Access to access your project\'s Locale service',
|
||||
],
|
||||
'avatars.read' => [
|
||||
'description' => 'Access to access your project\'s Avatars service',
|
||||
],
|
||||
'health.read' => [
|
||||
'description' => 'Access to read your project\'s health status',
|
||||
],
|
||||
'providers.read' => [
|
||||
'description' => 'Access to read your project\'s providers',
|
||||
],
|
||||
'providers.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s providers',
|
||||
],
|
||||
'messages.read' => [
|
||||
'description' => 'Access to read your project\'s messages',
|
||||
],
|
||||
'messages.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s messages',
|
||||
],
|
||||
'topics.read' => [
|
||||
'description' => 'Access to read your project\'s topics',
|
||||
],
|
||||
'topics.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s topics',
|
||||
],
|
||||
'subscribers.read' => [
|
||||
'description' => 'Access to read your project\'s subscribers',
|
||||
],
|
||||
'subscribers.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s subscribers',
|
||||
],
|
||||
'targets.read' => [
|
||||
'description' => 'Access to read your project\'s targets',
|
||||
],
|
||||
'targets.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s targets',
|
||||
],
|
||||
'rules.read' => [
|
||||
'description' => 'Access to read your project\'s proxy rules',
|
||||
],
|
||||
'rules.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s proxy rules',
|
||||
],
|
||||
'schedules.read' => [
|
||||
'description' => 'Access to read your project\'s schedules',
|
||||
],
|
||||
'schedules.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s schedules',
|
||||
],
|
||||
'migrations.read' => [
|
||||
'description' => 'Access to read your project\'s migrations',
|
||||
],
|
||||
'migrations.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s migrations.',
|
||||
],
|
||||
'vcs.read' => [
|
||||
'description' => 'Access to read your project\'s VCS repositories',
|
||||
],
|
||||
'vcs.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s VCS repositories',
|
||||
],
|
||||
'assistant.read' => [
|
||||
'description' => 'Access to read the Assistant service',
|
||||
],
|
||||
'tokens.read' => [
|
||||
'description' => 'Access to read your project\'s tokens',
|
||||
],
|
||||
'tokens.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s tokens',
|
||||
],
|
||||
"webhooks.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s webhooks",
|
||||
],
|
||||
"webhooks.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s webhooks",
|
||||
],
|
||||
// List of publicly visible scopes
|
||||
return [
|
||||
// Project
|
||||
"project.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s information",
|
||||
"category" => "Project",
|
||||
],
|
||||
"project.write" => [
|
||||
"description" =>
|
||||
"Access to update project\'s information",
|
||||
"category" => "Project",
|
||||
],
|
||||
"keys.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s keys",
|
||||
"category" => "Project",
|
||||
],
|
||||
"keys.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s keys",
|
||||
"category" => "Project",
|
||||
],
|
||||
"platforms.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s platforms",
|
||||
"category" => "Project",
|
||||
],
|
||||
"platforms.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s platforms",
|
||||
"category" => "Project",
|
||||
],
|
||||
"mocks.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s mocks",
|
||||
"category" => "Project",
|
||||
],
|
||||
"mocks.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s mocks",
|
||||
"category" => "Project",
|
||||
],
|
||||
"policies.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s policies",
|
||||
"category" => "Project",
|
||||
],
|
||||
"policies.write" => [
|
||||
"description" =>
|
||||
"Access to update project\'s policies",
|
||||
"category" => "Project",
|
||||
],
|
||||
"templates.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s templates",
|
||||
"category" => "Project",
|
||||
],
|
||||
"templates.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s templates",
|
||||
"category" => "Project",
|
||||
],
|
||||
"oauth2.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s OAuth2 configuration",
|
||||
"category" => "Project",
|
||||
],
|
||||
"oauth2.write" => [
|
||||
"description" =>
|
||||
"Access to update project\'s OAuth2 configuration",
|
||||
"category" => "Project",
|
||||
],
|
||||
|
||||
// Auth
|
||||
'users.read' => [
|
||||
'description' => 'Access to read users',
|
||||
'category' => 'Auth',
|
||||
],
|
||||
'users.write' => [
|
||||
'description' => 'Access to create, update, and delete users',
|
||||
'category' => 'Auth',
|
||||
],
|
||||
'sessions.read' => [
|
||||
'description' => 'Access to read user sessions',
|
||||
'category' => 'Auth',
|
||||
],
|
||||
'sessions.write' => [
|
||||
'description' => 'Access to create, update, and delete user sessions',
|
||||
'category' => 'Auth',
|
||||
],
|
||||
'teams.read' => [
|
||||
'description' => 'Access to read teams',
|
||||
'category' => 'Auth',
|
||||
],
|
||||
'teams.write' => [
|
||||
'description' => 'Access to create, update, and delete teams',
|
||||
'category' => 'Auth',
|
||||
],
|
||||
|
||||
// Databases
|
||||
'databases.read' => [
|
||||
'description' => 'Access to read databases',
|
||||
'category' => 'Databases',
|
||||
],
|
||||
'databases.write' => [
|
||||
'description' => 'Access to create, update, and delete databases',
|
||||
'category' => 'Databases',
|
||||
],
|
||||
'tables.read' => [
|
||||
'description' => 'Access to read database tables',
|
||||
'category' => 'Databases',
|
||||
],
|
||||
'tables.write' => [
|
||||
'description' => 'Access to create, update, and delete database tables',
|
||||
'category' => 'Databases',
|
||||
],
|
||||
'columns.read' => [
|
||||
'description' => 'Access to read database table columns',
|
||||
'category' => 'Databases',
|
||||
],
|
||||
'columns.write' => [
|
||||
'description' => 'Access to create, update, and delete database table columns',
|
||||
'category' => 'Databases',
|
||||
],
|
||||
'indexes.read' => [
|
||||
'description' => 'Access to read database table indexes',
|
||||
'category' => 'Databases',
|
||||
],
|
||||
'indexes.write' => [
|
||||
'description' => 'Access to create, update, and delete database table indexes',
|
||||
'category' => 'Databases',
|
||||
],
|
||||
'rows.read' => [
|
||||
'description' => 'Access to read database table rows',
|
||||
'category' => 'Databases',
|
||||
],
|
||||
'rows.write' => [
|
||||
'description' => 'Access to create, update, and delete database table rows',
|
||||
'category' => 'Databases',
|
||||
],
|
||||
'collections.read' => [
|
||||
'description' => 'Access to read database collections',
|
||||
'category' => 'Databases',
|
||||
'deprecated' => true,
|
||||
],
|
||||
'collections.write' => [
|
||||
'description' => 'Access to create, update, and delete database collections',
|
||||
'category' => 'Databases',
|
||||
'deprecated' => true,
|
||||
],
|
||||
'attributes.read' => [
|
||||
'description' => 'Access to read database collection attributes',
|
||||
'category' => 'Databases',
|
||||
'deprecated' => true,
|
||||
],
|
||||
'attributes.write' => [
|
||||
'description' => 'Access to create, update, and delete database collection attributes',
|
||||
'category' => 'Databases',
|
||||
'deprecated' => true,
|
||||
],
|
||||
'documents.read' => [
|
||||
'description' => 'Access to read database collection documents',
|
||||
'category' => 'Databases',
|
||||
'deprecated' => true,
|
||||
],
|
||||
'documents.write' => [
|
||||
'description' => 'Access to create, update, and delete database collection documents',
|
||||
'category' => 'Databases',
|
||||
'deprecated' => true,
|
||||
],
|
||||
|
||||
// Storage
|
||||
'buckets.read' => [
|
||||
'description' => 'Access to read storage buckets',
|
||||
'category' => 'Storage',
|
||||
],
|
||||
'buckets.write' => [
|
||||
'description' => 'Access to create, update, and delete storage buckets',
|
||||
'category' => 'Storage',
|
||||
],
|
||||
'files.read' => [
|
||||
'description' => 'Access to read storage files and preview images',
|
||||
'category' => 'Storage',
|
||||
],
|
||||
'files.write' => [
|
||||
'description' => 'Access to create, update, and delete storage files',
|
||||
'category' => 'Storage',
|
||||
],
|
||||
'tokens.read' => [
|
||||
'description' => 'Access to read storage file tokens',
|
||||
'category' => 'Storage',
|
||||
],
|
||||
'tokens.write' => [
|
||||
'description' => 'Access to create, update, and delete storage file tokens',
|
||||
'category' => 'Storage',
|
||||
],
|
||||
|
||||
// Functions
|
||||
'functions.read' => [
|
||||
'description' => 'Access to read functions and deployments',
|
||||
'category' => 'Functions',
|
||||
],
|
||||
'functions.write' => [
|
||||
'description' => 'Access to create, update, and delete functions and deployments',
|
||||
'category' => 'Functions',
|
||||
],
|
||||
'executions.read' => [
|
||||
'description' => 'Access to read function executions',
|
||||
'category' => 'Functions',
|
||||
],
|
||||
'executions.write' => [
|
||||
'description' => 'Access to create function executions',
|
||||
'category' => 'Functions',
|
||||
],
|
||||
|
||||
// Sites
|
||||
'sites.read' => [
|
||||
'description' => 'Access to read sites and deployments',
|
||||
'category' => 'Sites',
|
||||
],
|
||||
'sites.write' => [
|
||||
'description' => 'Access to create, update, and delete sites and deployments',
|
||||
'category' => 'Sites',
|
||||
],
|
||||
'log.read' => [
|
||||
'description' => 'Access to read site logs',
|
||||
'category' => 'Sites',
|
||||
],
|
||||
'log.write' => [
|
||||
'description' => 'Access to update, and delete site logs',
|
||||
'category' => 'Sites',
|
||||
],
|
||||
|
||||
// Messaging
|
||||
'providers.read' => [
|
||||
'description' => 'Access to read messaging providers',
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
'providers.write' => [
|
||||
'description' => 'Access to create, update, and delete messaging providers',
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
'topics.read' => [
|
||||
'description' => 'Access to read messaging topics',
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
'topics.write' => [
|
||||
'description' => 'Access to create, update, and delete messaging topics',
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
'subscribers.read' => [
|
||||
'description' => 'Access to read messaging subscribers',
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
'subscribers.write' => [
|
||||
'description' => 'Access to create, update, and delete messaging subscribers',
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
'targets.read' => [
|
||||
'description' => 'Access to read messaging targets',
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
'targets.write' => [
|
||||
'description' => 'Access to create, update, and delete messaging targets',
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
'messages.read' => [
|
||||
'description' => 'Access to read messaging messages',
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
'messages.write' => [
|
||||
'description' => 'Access to create, update, and delete messaging messages',
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
|
||||
// Other
|
||||
"webhooks.read" => [
|
||||
"description" =>
|
||||
"Access to read webhooks",
|
||||
'category' => 'Other',
|
||||
],
|
||||
"webhooks.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete webhooks",
|
||||
'category' => 'Other',
|
||||
],
|
||||
'locale.read' => [
|
||||
'description' => 'Access to use Locale service',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'avatars.read' => [
|
||||
'description' => 'Access to use Avatars service',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'health.read' => [
|
||||
'description' => 'Access to use Health service',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'assistant.read' => [
|
||||
'description' => 'Access to use Assistant service',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'migrations.read' => [
|
||||
'description' => 'Access to read migrations',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'migrations.write' => [
|
||||
'description' => 'Access to create, update, and delete migrations.',
|
||||
'category' => 'Other',
|
||||
],
|
||||
|
||||
// TODO: Figure out where to move those
|
||||
'schedules.read' => [
|
||||
'description' => 'Access to read schedules.',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'schedules.write' => [
|
||||
'description' => 'Access to create, update, and delete schedules.',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'vcs.read' => [
|
||||
'description' => 'Access to read resources under VCS service.',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'vcs.write' => [
|
||||
'description' => 'Access to create, update, and delete resources under VCS service.',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'rules.read' => [
|
||||
'description' => 'Access to read proxy rules.',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'rules.write' => [
|
||||
'description' => 'Access to create, update, and delete proxy rules.',
|
||||
'category' => 'Other',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -286,7 +286,7 @@ return [
|
||||
'name' => 'Migrations',
|
||||
'subtitle' => 'The Migrations service allows you to migrate third-party data to your Appwrite project.',
|
||||
'description' => '/docs/services/migrations.md',
|
||||
'controller' => 'api/migrations.php',
|
||||
'controller' => '', // Uses modules
|
||||
'sdk' => true,
|
||||
'docs' => true,
|
||||
'docsUrl' => 'https://appwrite.io/docs/migrations',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -856,7 +856,7 @@ Http::get('/v1/users/:userId/targets/:targetId')
|
||||
Http::get('/v1/users/:userId/sessions')
|
||||
->desc('List user sessions')
|
||||
->groups(['api', 'users'])
|
||||
->label('scope', 'users.read')
|
||||
->label('scope', ['users.read', 'sessions.read'])
|
||||
->label('sdk', new Method(
|
||||
namespace: 'users',
|
||||
group: 'sessions',
|
||||
@@ -2314,7 +2314,7 @@ Http::post('/v1/users/:userId/sessions')
|
||||
->desc('Create session')
|
||||
->groups(['api', 'users'])
|
||||
->label('event', 'users.[userId].sessions.[sessionId].create')
|
||||
->label('scope', 'users.write')
|
||||
->label('scope', ['users.write', 'sessions.write'])
|
||||
->label('audits.event', 'session.create')
|
||||
->label('audits.resource', 'user/{request.userId}')
|
||||
->label('usage.metric', 'sessions.{scope}.requests.create')
|
||||
@@ -2470,7 +2470,7 @@ Http::delete('/v1/users/:userId/sessions/:sessionId')
|
||||
->desc('Delete user session')
|
||||
->groups(['api', 'users'])
|
||||
->label('event', 'users.[userId].sessions.[sessionId].delete')
|
||||
->label('scope', 'users.write')
|
||||
->label('scope', ['users.write', 'sessions.write'])
|
||||
->label('audits.event', 'session.delete')
|
||||
->label('audits.resource', 'user/{request.userId}')
|
||||
->label('sdk', new Method(
|
||||
@@ -2521,7 +2521,7 @@ Http::delete('/v1/users/:userId/sessions')
|
||||
->desc('Delete user sessions')
|
||||
->groups(['api', 'users'])
|
||||
->label('event', 'users.[userId].sessions.delete')
|
||||
->label('scope', 'users.write')
|
||||
->label('scope', ['users.write', 'sessions.write'])
|
||||
->label('audits.event', 'session.delete')
|
||||
->label('audits.resource', 'user/{user.$id}')
|
||||
->label('sdk', new Method(
|
||||
|
||||
+25
-12
@@ -27,6 +27,7 @@ use Appwrite\Utopia\Request\Filters\V20 as RequestV20;
|
||||
use Appwrite\Utopia\Request\Filters\V21 as RequestV21;
|
||||
use Appwrite\Utopia\Request\Filters\V22 as RequestV22;
|
||||
use Appwrite\Utopia\Request\Filters\V23 as RequestV23;
|
||||
use Appwrite\Utopia\Request\Filters\V24 as RequestV24;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
|
||||
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
|
||||
@@ -36,6 +37,7 @@ use Appwrite\Utopia\Response\Filters\V20 as ResponseV20;
|
||||
use Appwrite\Utopia\Response\Filters\V21 as ResponseV21;
|
||||
use Appwrite\Utopia\Response\Filters\V22 as ResponseV22;
|
||||
use Appwrite\Utopia\Response\Filters\V23 as ResponseV23;
|
||||
use Appwrite\Utopia\Response\Filters\V24 as ResponseV24;
|
||||
use Appwrite\Utopia\View;
|
||||
use Executor\Executor;
|
||||
use MaxMind\Db\Reader;
|
||||
@@ -54,7 +56,6 @@ use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Domains\Domain;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Http\RouteMatch;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\Logger\Adapter\Sentry;
|
||||
use Utopia\Logger\Log;
|
||||
@@ -398,7 +399,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
'projectId' => $project->getId(),
|
||||
'scopes' => $resource->getAttribute('scopes', [])
|
||||
]);
|
||||
$headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $jwtKey;
|
||||
$headers['x-appwrite-key'] = API_KEY_EPHEMERAL . '_' . $jwtKey;
|
||||
$headers['x-appwrite-trigger'] = 'http';
|
||||
$headers['x-appwrite-user-jwt'] = '';
|
||||
|
||||
@@ -848,9 +849,7 @@ Http::init()
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->inject('match')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount, ?RouteMatch $match) {
|
||||
$route = $match?->route;
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
@@ -858,12 +857,15 @@ Http::init()
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
// Only run Router when external domain
|
||||
if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
|
||||
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount);
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Request format
|
||||
*/
|
||||
$route = $utopia->getRoute();
|
||||
$request->setRoute($route);
|
||||
|
||||
if ($route === null) {
|
||||
@@ -899,6 +901,9 @@ Http::init()
|
||||
if (version_compare($requestFormat, '1.9.2', '<')) {
|
||||
$request->addFilter(new RequestV23());
|
||||
}
|
||||
if (version_compare($requestFormat, '1.9.3', '<')) {
|
||||
$request->addFilter(new RequestV24());
|
||||
}
|
||||
}
|
||||
|
||||
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
|
||||
@@ -923,6 +928,9 @@ Http::init()
|
||||
*/
|
||||
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
|
||||
if ($responseFormat) {
|
||||
if (version_compare($responseFormat, '1.9.3', '<')) {
|
||||
$response->addFilter(new ResponseV24());
|
||||
}
|
||||
if (version_compare($responseFormat, '1.9.2', '<')) {
|
||||
$response->addFilter(new ResponseV23());
|
||||
}
|
||||
@@ -1147,7 +1155,9 @@ Http::options()
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
// Only run Router when external domain
|
||||
if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
|
||||
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount);
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
|
||||
@@ -1179,10 +1189,9 @@ Http::error()
|
||||
->inject('bus')
|
||||
->inject('devKey')
|
||||
->inject('authorization')
|
||||
->inject('match')
|
||||
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization, ?RouteMatch $match) {
|
||||
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) {
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
$route = $match?->route;
|
||||
$route = $utopia->getRoute();
|
||||
$class = \get_class($error);
|
||||
$code = $error->getCode();
|
||||
$message = $error->getMessage();
|
||||
@@ -1547,7 +1556,9 @@ Http::get('/robots.txt')
|
||||
$template = new View(__DIR__ . '/../views/general/robots.phtml');
|
||||
$response->text($template->render(false));
|
||||
} else {
|
||||
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount);
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1579,7 +1590,9 @@ Http::get('/humans.txt')
|
||||
$template = new View(__DIR__ . '/../views/general/humans.phtml');
|
||||
$response->text($template->render(false));
|
||||
} else {
|
||||
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount);
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Http\RouteMatch;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\Text;
|
||||
@@ -287,11 +286,10 @@ Http::shutdown()
|
||||
->inject('utopia')
|
||||
->inject('response')
|
||||
->inject('request')
|
||||
->inject('match')
|
||||
->action(function (Http $utopia, Response $response, Request $request, ?RouteMatch $match) {
|
||||
$route = $match?->route;
|
||||
->action(function (Http $utopia, Response $response, Request $request) {
|
||||
|
||||
$result = [];
|
||||
$route = $utopia->getRoute();
|
||||
$path = APP_STORAGE_CACHE . '/tests.json';
|
||||
$tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : [];
|
||||
|
||||
|
||||
@@ -39,8 +39,6 @@ use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Authorization\Input;
|
||||
use Utopia\Database\Validator\Roles;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Http\Route;
|
||||
use Utopia\Http\RouteMatch;
|
||||
use Utopia\Span\Span;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter as Telemetry;
|
||||
@@ -102,9 +100,8 @@ Http::init()
|
||||
->inject('team')
|
||||
->inject('apiKey')
|
||||
->inject('authorization')
|
||||
->inject('match')
|
||||
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization, ?RouteMatch $match) {
|
||||
$route = $match?->route;
|
||||
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
|
||||
$route = $utopia->getRoute();
|
||||
if ($route === null) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
|
||||
}
|
||||
@@ -186,7 +183,8 @@ Http::init()
|
||||
// Handle special app role case
|
||||
if ($apiKey->getRole() === User::ROLE_APPS) {
|
||||
// Disable authorization checks for project API keys
|
||||
if (($apiKey->getType() === API_KEY_STANDARD || $apiKey->getType() === API_KEY_DYNAMIC) && $apiKey->getProjectId() === $project->getId()) {
|
||||
// Dynamic supported for backwards compatibility
|
||||
if (($apiKey->getType() === API_KEY_STANDARD || $apiKey->getType() === API_KEY_EPHEMERAL || $apiKey->getType() === 'dynamic') && $apiKey->getProjectId() === $project->getId()) {
|
||||
$authorization->setDefaultStatus(false);
|
||||
}
|
||||
|
||||
@@ -505,19 +503,17 @@ Http::init()
|
||||
->inject('telemetry')
|
||||
->inject('platform')
|
||||
->inject('authorization')
|
||||
->inject('match')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization, ?RouteMatch $match) {
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
|
||||
|
||||
$response->setUser($user);
|
||||
$request->setUser($user);
|
||||
|
||||
$route = $match?->route;
|
||||
$route = $utopia->getRoute();
|
||||
if ($route === null) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
|
||||
}
|
||||
$path = $match->path;
|
||||
|
||||
|
||||
$path = $route->getMatchedPath();
|
||||
$databaseType = match (true) {
|
||||
str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
|
||||
str_contains($path, '/vectorsdb') => DATABASE_TYPE_VECTORSDB,
|
||||
@@ -633,6 +629,7 @@ Http::init()
|
||||
$useCache = $route->getLabel('cache', false);
|
||||
$storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load');
|
||||
if ($useCache) {
|
||||
$route = $utopia->match($request);
|
||||
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
|
||||
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
@@ -815,9 +812,7 @@ Http::shutdown()
|
||||
->inject('bus')
|
||||
->inject('apiKey')
|
||||
->inject('mode')
|
||||
->inject('match')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode, ?RouteMatch $match) use ($parseLabel) {
|
||||
$route = $match?->route;
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
|
||||
|
||||
$responsePayload = $response->getPayload();
|
||||
|
||||
@@ -867,7 +862,8 @@ Http::shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
$requestParams = $match->arguments ?? [];
|
||||
$route = $utopia->getRoute();
|
||||
$requestParams = $route->getParamsValues();
|
||||
|
||||
/**
|
||||
* Abuse labels
|
||||
|
||||
@@ -9,7 +9,6 @@ use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Http\RouteMatch;
|
||||
use Utopia\System\System;
|
||||
|
||||
Http::init()
|
||||
@@ -33,13 +32,13 @@ Http::init()
|
||||
|
||||
Http::init()
|
||||
->groups(['auth'])
|
||||
->inject('utopia')
|
||||
->inject('request')
|
||||
->inject('project')
|
||||
->inject('geodb')
|
||||
->inject('user')
|
||||
->inject('authorization')
|
||||
->inject('match')
|
||||
->action(function (Request $request, Document $project, Reader $geodb, User $user, Authorization $authorization, ?RouteMatch $match) {
|
||||
->action(function (Http $utopia, Request $request, Document $project, Reader $geodb, User $user, Authorization $authorization) {
|
||||
$denylist = System::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', '');
|
||||
if (!empty($denylist && $project->getId() === 'console')) {
|
||||
$countries = explode(',', $denylist);
|
||||
@@ -50,7 +49,7 @@ Http::init()
|
||||
}
|
||||
}
|
||||
|
||||
$route = $match?->route;
|
||||
$route = $utopia->match($request);
|
||||
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
$isAppUser = $user->isApp($authorization->getRoles());
|
||||
|
||||
+12
-12
@@ -3,7 +3,7 @@
|
||||
require_once __DIR__ . '/init.php';
|
||||
require_once __DIR__ . '/init/span.php';
|
||||
|
||||
$registerContext = require __DIR__ . '/init/resources/request.php';
|
||||
$registerRequestResources = require __DIR__ . '/init/resources/request.php';
|
||||
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
@@ -191,7 +191,7 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
|
||||
});
|
||||
|
||||
$container->set('bus', function ($register) use ($swooleAdapter) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $swooleAdapter->getContext()->get($name));
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $swooleAdapter->getContainer()->get($name));
|
||||
}, ['register']);
|
||||
|
||||
include __DIR__ . '/controllers/general.php';
|
||||
@@ -502,7 +502,7 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke
|
||||
});
|
||||
});
|
||||
|
||||
$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swooleAdapter, $registerContext) {
|
||||
$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swooleAdapter, $registerRequestResources) {
|
||||
Span::init('http.request');
|
||||
|
||||
$request = new Request($utopiaRequest->getSwooleRequest());
|
||||
@@ -522,15 +522,15 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
|
||||
return;
|
||||
}
|
||||
|
||||
$context = $swooleAdapter->getContext();
|
||||
$context->set('container', fn () => $context);
|
||||
$context->set('request', fn () => $request);
|
||||
$context->set('response', fn () => $response);
|
||||
$requestContainer = $swooleAdapter->getContainer();
|
||||
$requestContainer->set('container', fn () => $requestContainer);
|
||||
$requestContainer->set('request', fn () => $request);
|
||||
$requestContainer->set('response', fn () => $response);
|
||||
|
||||
$app = new Http($swooleAdapter, 'UTC');
|
||||
$context->set('utopia', fn () => $app);
|
||||
$requestContainer->set('utopia', fn () => $app);
|
||||
|
||||
$registerContext($context);
|
||||
$registerRequestResources($requestContainer);
|
||||
|
||||
$app->setCompression(System::getEnv('_APP_COMPRESSION_ENABLED', 'enabled') === 'enabled');
|
||||
$app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB
|
||||
@@ -545,8 +545,8 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
|
||||
|
||||
$app->run($request, $response);
|
||||
|
||||
$match = $app->getResource('match');
|
||||
Span::add('http.path', $match?->route->getPath() ?? 'unknown');
|
||||
$route = $app->getRoute();
|
||||
Span::add('http.path', $route?->getPath() ?? 'unknown');
|
||||
} catch (\Throwable $th) {
|
||||
Span::error($th);
|
||||
|
||||
@@ -561,7 +561,7 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
|
||||
// All good, user is optional information for logger
|
||||
}
|
||||
|
||||
$route = $app->getResource('match')?->route;
|
||||
$route = $app->getRoute();
|
||||
|
||||
$log = $app->getResource("log");
|
||||
|
||||
|
||||
+11
-3
@@ -44,8 +44,8 @@ const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
|
||||
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 = 4323;
|
||||
const APP_VERSION_STABLE = '1.9.2';
|
||||
const APP_CACHE_BUSTER = 4324;
|
||||
const APP_VERSION_STABLE = '1.9.3';
|
||||
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
|
||||
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
|
||||
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
|
||||
@@ -244,6 +244,7 @@ const APP_AUTH_TYPE_KEY = 'Key';
|
||||
const APP_AUTH_TYPE_ADMIN = 'Admin';
|
||||
// Response related
|
||||
const MAX_OUTPUT_CHUNK_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
const APP_LIMIT_UPLOAD_CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
const APP_FUNCTION_LOG_LENGTH_LIMIT = 1000000;
|
||||
const APP_FUNCTION_ERROR_LENGTH_LIMIT = 1000000;
|
||||
// Function headers
|
||||
@@ -253,9 +254,16 @@ const FUNCTION_ALLOWLIST_HEADERS_RESPONSE = ['content-type', 'content-length'];
|
||||
const MESSAGE_TYPE_EMAIL = 'email';
|
||||
const MESSAGE_TYPE_SMS = 'sms';
|
||||
const MESSAGE_TYPE_PUSH = 'push';
|
||||
// Notification types
|
||||
const NOTIFICATION_TYPE_EMAIL = 'email';
|
||||
const NOTIFICATION_TYPE_SMS = 'sms';
|
||||
const NOTIFICATION_TYPE_PUSH = 'push';
|
||||
const NOTIFICATION_TYPE_CONSOLE = 'console';
|
||||
const NOTIFICATION_TYPE_WEBHOOK = 'webhook';
|
||||
const RESOURCE_TYPE_ALERTS = 'alerts';
|
||||
// API key types
|
||||
const API_KEY_STANDARD = 'standard';
|
||||
const API_KEY_DYNAMIC = 'dynamic';
|
||||
const API_KEY_EPHEMERAL = 'ephemeral';
|
||||
const API_KEY_ORGANIZATION = 'organization';
|
||||
const API_KEY_ACCOUNT = 'account';
|
||||
// Usage metrics
|
||||
|
||||
@@ -56,6 +56,8 @@ use Appwrite\Utopia\Response\Model\ColumnString;
|
||||
use Appwrite\Utopia\Response\Model\ColumnText;
|
||||
use Appwrite\Utopia\Response\Model\ColumnURL;
|
||||
use Appwrite\Utopia\Response\Model\ColumnVarchar;
|
||||
use Appwrite\Utopia\Response\Model\ConsoleKeyScope;
|
||||
use Appwrite\Utopia\Response\Model\ConsoleKeyScopeList;
|
||||
use Appwrite\Utopia\Response\Model\ConsoleOAuth2Provider;
|
||||
use Appwrite\Utopia\Response\Model\ConsoleOAuth2ProviderList;
|
||||
use Appwrite\Utopia\Response\Model\ConsoleOAuth2ProviderParameter;
|
||||
@@ -71,6 +73,7 @@ use Appwrite\Utopia\Response\Model\DetectionVariable;
|
||||
use Appwrite\Utopia\Response\Model\DevKey;
|
||||
use Appwrite\Utopia\Response\Model\Document as ModelDocument;
|
||||
use Appwrite\Utopia\Response\Model\Embedding;
|
||||
use Appwrite\Utopia\Response\Model\EphemeralKey;
|
||||
use Appwrite\Utopia\Response\Model\Error;
|
||||
use Appwrite\Utopia\Response\Model\ErrorDev;
|
||||
use Appwrite\Utopia\Response\Model\Execution;
|
||||
@@ -392,6 +395,7 @@ Response::setModel(new Execution());
|
||||
Response::setModel(new Project());
|
||||
Response::setModel(new Webhook());
|
||||
Response::setModel(new Key());
|
||||
Response::setModel(new EphemeralKey());
|
||||
Response::setModel(new DevKey());
|
||||
Response::setModel(new MockNumber());
|
||||
Response::setModel(new OAuth2GitHub());
|
||||
@@ -486,6 +490,8 @@ Response::setModel(new ConsoleVariables());
|
||||
Response::setModel(new ConsoleOAuth2ProviderParameter());
|
||||
Response::setModel(new ConsoleOAuth2Provider());
|
||||
Response::setModel(new ConsoleOAuth2ProviderList());
|
||||
Response::setModel(new ConsoleKeyScope());
|
||||
Response::setModel(new ConsoleKeyScopeList());
|
||||
Response::setModel(new MFAChallenge());
|
||||
Response::setModel(new MFARecoveryCodes());
|
||||
Response::setModel(new MFAType());
|
||||
|
||||
@@ -327,9 +327,11 @@ return function (Container $container): void {
|
||||
}
|
||||
}
|
||||
|
||||
$impersonateUserId = $request->getHeader('x-appwrite-impersonate-user-id', '');
|
||||
$impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', '');
|
||||
$impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', '');
|
||||
// Query params mirror the header fallback pattern used by ?project= and ?devKey=,
|
||||
// allowing Console to embed impersonation in direct file/image URLs where headers cannot be set.
|
||||
$impersonateUserId = $request->getHeader('x-appwrite-impersonate-user-id', (string)$request->getParam('impersonateUserId', ''));
|
||||
$impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', (string)$request->getParam('impersonateEmail', ''));
|
||||
$impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', (string)$request->getParam('impersonatePhone', ''));
|
||||
|
||||
if (!$user->isEmpty() && $user->getAttribute('impersonator', false)) {
|
||||
$userDb = ($mode === APP_MODE_ADMIN || $project->getId() === 'console') ? $dbForPlatform : $dbForProject;
|
||||
|
||||
@@ -159,10 +159,16 @@ $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization
|
||||
$adapter = new DatabasePool($pools->get('logs'));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$logsCollections = $collections['logs'] ?? [];
|
||||
$logsCollections = array_keys($logsCollections);
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($logsCollections)
|
||||
->setNamespace('logsV1')
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
|
||||
|
||||
@@ -47,7 +47,6 @@ use Utopia\DI\Container;
|
||||
use Utopia\Domains\Domain;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Http\RouteMatch;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Pools\Group;
|
||||
@@ -205,9 +204,16 @@ return function (Container $container): void {
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$projectCollections = $collections['projects'] ?? [];
|
||||
$projectsGlobalCollections = array_keys($projectCollections);
|
||||
$projectsGlobalCollections[] = 'audit';
|
||||
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
@@ -224,6 +230,11 @@ return function (Container $container): void {
|
||||
$adapter = null;
|
||||
|
||||
return function (?Document $project = null) use ($pools, $cache, $authorization, &$adapter) {
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$logsCollections = $collections['logs'] ?? [];
|
||||
$logsCollections = array_keys($logsCollections);
|
||||
|
||||
$adapter ??= new DatabasePool($pools->get('logs'));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
@@ -231,6 +242,7 @@ return function (Container $container): void {
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($logsCollections)
|
||||
->setNamespace('logsV1')
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
|
||||
@@ -572,10 +584,12 @@ return function (Container $container): void {
|
||||
}
|
||||
}
|
||||
|
||||
// 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', '');
|
||||
// Impersonation: if current user has impersonator capability and headers/params are set, act as another user
|
||||
// Query params mirror the header fallback pattern used by ?project= and ?devKey=,
|
||||
// allowing Console to embed impersonation in direct file/image URLs where headers cannot be set.
|
||||
$impersonateUserId = $request->getHeader('x-appwrite-impersonate-user-id', (string)$request->getParam('impersonateUserId', ''));
|
||||
$impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', (string)$request->getParam('impersonateEmail', ''));
|
||||
$impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', (string)$request->getParam('impersonatePhone', ''));
|
||||
if (!$user->isEmpty() && $user->getAttribute('impersonator', false)) {
|
||||
$userDb = (APP_MODE_ADMIN === $mode || $project->getId() === 'console') ? $dbForPlatform : $dbForProject;
|
||||
$targetUser = null;
|
||||
@@ -603,7 +617,7 @@ return function (Container $container): void {
|
||||
return $user;
|
||||
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']);
|
||||
|
||||
$container->set('project', function ($dbForPlatform, $request, $console, $authorization, ?RouteMatch $match) {
|
||||
$container->set('project', function ($dbForPlatform, $request, $console, $authorization, Http $utopia) {
|
||||
/** @var Appwrite\Utopia\Request $request */
|
||||
/** @var Utopia\Database\Database $dbForPlatform */
|
||||
/** @var Utopia\Database\Document $console */
|
||||
@@ -617,7 +631,7 @@ return function (Container $container): void {
|
||||
// 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 = $match?->route;
|
||||
$route = $utopia->match($request);
|
||||
if (!empty($route)) {
|
||||
$isDeprecatedAlias = \str_starts_with($request->getURI(), $deprecatedProjectPathPrefix) &&
|
||||
!\str_starts_with($route->getPath(), $deprecatedProjectPathPrefix);
|
||||
@@ -634,7 +648,7 @@ return function (Container $container): void {
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
|
||||
|
||||
return $project;
|
||||
}, ['dbForPlatform', 'request', 'console', 'authorization', 'match']);
|
||||
}, ['dbForPlatform', 'request', 'console', 'authorization', 'utopia']);
|
||||
|
||||
$container->set('session', function (User $user, Store $store, Token $proofForToken) {
|
||||
if ($user->isEmpty()) {
|
||||
@@ -689,8 +703,15 @@ return function (Container $container): void {
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$projectCollections = $collections['projects'] ?? [];
|
||||
$projectsGlobalCollections = array_keys($projectCollections);
|
||||
$projectsGlobalCollections[] = 'audit';
|
||||
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
@@ -1101,12 +1122,12 @@ return function (Container $container): void {
|
||||
return $key;
|
||||
}, ['request', 'project', 'servers', 'dbForPlatform', 'authorization']);
|
||||
|
||||
$container->set('team', function (Document $project, Database $dbForPlatform, ?RouteMatch $match, Request $request, Authorization $authorization) {
|
||||
$container->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) {
|
||||
$teamInternalId = '';
|
||||
if ($project->getId() !== 'console') {
|
||||
$teamInternalId = $project->getAttribute('teamInternalId', '');
|
||||
} else {
|
||||
$route = $match?->route;
|
||||
$route = $utopia->match($request);
|
||||
$path = ! empty($route) ? $route->getPath() : $request->getURI();
|
||||
$orgHeader = $request->getHeader('x-appwrite-organization', '');
|
||||
if (str_starts_with($path, '/v1/projects/:projectId')) {
|
||||
@@ -1142,7 +1163,7 @@ return function (Container $container): void {
|
||||
});
|
||||
|
||||
return $team;
|
||||
}, ['project', 'dbForPlatform', 'match', 'request', 'authorization']);
|
||||
}, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization']);
|
||||
|
||||
$container->set('previewHostname', function (Request $request, ?Key $apiKey) {
|
||||
$allowed = false;
|
||||
@@ -1291,6 +1312,12 @@ return function (Container $container): void {
|
||||
$database = new Database($adapter, $cache);
|
||||
$sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')));
|
||||
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$projectCollections = $collections['projects'] ?? [];
|
||||
$projectsGlobalCollections = array_keys($projectCollections);
|
||||
$projectsGlobalCollections[] = 'audit';
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
@@ -1313,6 +1340,7 @@ return function (Container $container): void {
|
||||
if (\in_array($databaseHost, $dbTypeSharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($databaseDSN->getParam('namespace'));
|
||||
} else {
|
||||
@@ -1324,6 +1352,7 @@ return function (Container $container): void {
|
||||
} elseif (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
|
||||
@@ -14,6 +14,7 @@ use Appwrite\Utopia\Database\Documents\User;
|
||||
use Utopia\Audit\Adapter\Database as AdapterDatabase;
|
||||
use Utopia\Audit\Audit as UtopiaAudit;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Adapter\Pool as DatabasePool;
|
||||
use Utopia\Database\Database;
|
||||
@@ -90,8 +91,15 @@ return function (Container $container): void {
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$projectCollections = $collections['projects'] ?? [];
|
||||
$projectsGlobalCollections = array_keys($projectCollections);
|
||||
$projectsGlobalCollections[] = 'audit';
|
||||
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
@@ -130,8 +138,15 @@ return function (Container $container): void {
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$projectCollections = $collections['projects'] ?? [];
|
||||
$projectsGlobalCollections = array_keys($projectCollections);
|
||||
$projectsGlobalCollections[] = 'audit';
|
||||
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
@@ -152,8 +167,15 @@ return function (Container $container): void {
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$projectCollections = $collections['projects'] ?? [];
|
||||
$projectsGlobalCollections = array_keys($projectCollections);
|
||||
$projectsGlobalCollections[] = 'audit';
|
||||
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
@@ -210,6 +232,14 @@ return function (Container $container): void {
|
||||
|
||||
$sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')));
|
||||
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$projectCollections = $collections['projects'] ?? [];
|
||||
$projectsGlobalCollections = array_keys($projectCollections);
|
||||
$projectsGlobalCollections[] = 'audit';
|
||||
|
||||
$database->setGlobalCollections($projectsGlobalCollections);
|
||||
|
||||
// For separate pools (documentsdb/vectorsdb), check their own shared tables config.
|
||||
// If not configured, use dedicated mode to avoid cross-engine tenant type mismatches.
|
||||
if ($databaseHost !== $dsn->getHost()) {
|
||||
@@ -222,6 +252,7 @@ return function (Container $container): void {
|
||||
if (\in_array($databaseHost, $dbTypeSharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setTenant($projectDocument->getSequence())
|
||||
->setNamespace($databaseDSN->getParam('namespace'));
|
||||
} else {
|
||||
@@ -233,6 +264,7 @@ return function (Container $container): void {
|
||||
} elseif (\in_array($dsn->getHost(), $sharedTables, true)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setTenant($projectDocument->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
@@ -257,6 +289,11 @@ return function (Container $container): void {
|
||||
return $database;
|
||||
}
|
||||
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$logsCollections = $collections['logs'] ?? [];
|
||||
$logsCollections = array_keys($logsCollections);
|
||||
|
||||
$adapter = new DatabasePool($pools->get('logs'));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
@@ -264,6 +301,7 @@ return function (Container $container): void {
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($logsCollections)
|
||||
->setNamespace('logsV1')
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
|
||||
|
||||
+30
-4
@@ -45,7 +45,10 @@ use Utopia\WebSocket\Adapter;
|
||||
use Utopia\WebSocket\Server;
|
||||
|
||||
require_once __DIR__ . '/init.php';
|
||||
require_once __DIR__ . '/init/span.php';
|
||||
|
||||
if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') {
|
||||
require_once __DIR__ . '/init/span.php';
|
||||
}
|
||||
|
||||
/** @var Registry $register */
|
||||
$register = $GLOBALS['register'] ?? throw new \RuntimeException('Registry not initialized');
|
||||
@@ -127,8 +130,14 @@ if (!function_exists('getProjectDB')) {
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$collections = Config::getParam('collections', []);
|
||||
$projectCollections = $collections['projects'] ?? [];
|
||||
$projectsGlobalCollections = array_keys($projectCollections);
|
||||
$projectsGlobalCollections[] = 'audit';
|
||||
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
@@ -566,6 +575,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
|
||||
$roles = $user->getRoles($database->getAuthorization());
|
||||
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
|
||||
$previousUserId = $realtime->connections[$connection]['userId'] ?? '';
|
||||
|
||||
$meta = $realtime->getSubscriptionMetadata($connection);
|
||||
|
||||
@@ -573,13 +583,19 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
|
||||
foreach ($meta as $subscriptionId => $subscription) {
|
||||
$queries = Query::parseQueries($subscription['queries'] ?? []);
|
||||
$channels = Realtime::rebindAccountChannels(
|
||||
$subscription['channels'] ?? [],
|
||||
$previousUserId,
|
||||
$userId
|
||||
);
|
||||
$realtime->subscribe(
|
||||
$projectId,
|
||||
$connection,
|
||||
$subscriptionId,
|
||||
$roles,
|
||||
$subscription['channels'] ?? [],
|
||||
$queries
|
||||
$channels,
|
||||
$queries,
|
||||
$userId
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1068,6 +1084,11 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
|
||||
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
|
||||
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
|
||||
// Capture the pre-auth userId so we can rebind any account channels
|
||||
// that were stored under it (e.g. guest who subscribed to `account`
|
||||
// and now authenticates). unsubscribe() below clears the connection
|
||||
// entry, so we must read it first.
|
||||
$previousUserId = $realtime->connections[$connection]['userId'] ?? '';
|
||||
|
||||
$subscriptionsBefore = \count($realtime->getSubscriptionMetadata($connection));
|
||||
$meta = $realtime->getSubscriptionMetadata($connection);
|
||||
@@ -1077,13 +1098,18 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
if (!empty($projectId)) {
|
||||
foreach ($meta as $subscriptionId => $subscription) {
|
||||
$queries = Query::parseQueries($subscription['queries'] ?? []);
|
||||
$channels = Realtime::rebindAccountChannels(
|
||||
$subscription['channels'] ?? [],
|
||||
$previousUserId,
|
||||
$user->getId()
|
||||
);
|
||||
|
||||
$realtime->subscribe(
|
||||
$projectId,
|
||||
$connection,
|
||||
$subscriptionId,
|
||||
$roles,
|
||||
$subscription['channels'] ?? [],
|
||||
$channels,
|
||||
$queries,
|
||||
$user->getId()
|
||||
);
|
||||
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
exec php /usr/src/code/app/worker.php notifications "$@"
|
||||
+2
-2
@@ -67,7 +67,7 @@
|
||||
"utopia-php/emails": "0.6.*",
|
||||
"utopia-php/dns": "1.6.*",
|
||||
"utopia-php/dsn": "0.2.1",
|
||||
"utopia-php/http": "dev-fix/concurrency-shared-state as 0.34.99",
|
||||
"utopia-php/http": "0.34.*",
|
||||
"utopia-php/fetch": "0.5.*",
|
||||
"utopia-php/validators": "0.2.*",
|
||||
"utopia-php/image": "0.8.*",
|
||||
@@ -82,7 +82,7 @@
|
||||
"utopia-php/queue": "0.17.*",
|
||||
"utopia-php/servers": "0.3.*",
|
||||
"utopia-php/registry": "0.5.*",
|
||||
"utopia-php/storage": "1.0.*",
|
||||
"utopia-php/storage": "2.*",
|
||||
"utopia-php/system": "0.10.*",
|
||||
"utopia-php/telemetry": "0.2.*",
|
||||
"utopia-php/vcs": "3.*",
|
||||
|
||||
Generated
+53
-63
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "4fe55bdcb4efe2a8e4394e3a504bd94e",
|
||||
"content-hash": "4bee36b21a57e754d2b3417e72dc9599",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -3351,16 +3351,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/abuse",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/abuse.git",
|
||||
"reference": "20bee84fd14dbe81d50ecabf1ffd81cceca06152"
|
||||
"reference": "53f4274939353522ba331f55bcff6e6011ffc56c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/20bee84fd14dbe81d50ecabf1ffd81cceca06152",
|
||||
"reference": "20bee84fd14dbe81d50ecabf1ffd81cceca06152",
|
||||
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/53f4274939353522ba331f55bcff6e6011ffc56c",
|
||||
"reference": "53f4274939353522ba331f55bcff6e6011ffc56c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3397,9 +3397,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/abuse/issues",
|
||||
"source": "https://github.com/utopia-php/abuse/tree/1.2.2"
|
||||
"source": "https://github.com/utopia-php/abuse/tree/1.2.3"
|
||||
},
|
||||
"time": "2026-02-02T10:43:10+00:00"
|
||||
"time": "2026-04-29T11:19:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/agents",
|
||||
@@ -3850,16 +3850,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/database",
|
||||
"version": "5.3.22",
|
||||
"version": "5.4.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/database.git",
|
||||
"reference": "d765945da6b3141852014b2f96ecf1fe7e3d6ba7"
|
||||
"reference": "688d9422b5ff42ac2ecc29397d94891cfd772e93"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/d765945da6b3141852014b2f96ecf1fe7e3d6ba7",
|
||||
"reference": "d765945da6b3141852014b2f96ecf1fe7e3d6ba7",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/688d9422b5ff42ac2ecc29397d94891cfd772e93",
|
||||
"reference": "688d9422b5ff42ac2ecc29397d94891cfd772e93",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3903,9 +3903,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/database/issues",
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.22"
|
||||
"source": "https://github.com/utopia-php/database/tree/5.4.1"
|
||||
},
|
||||
"time": "2026-04-20T07:12:46+00:00"
|
||||
"time": "2026-04-29T07:32:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/detector",
|
||||
@@ -4062,16 +4062,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/domains",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/domains.git",
|
||||
"reference": "0edf6bb2b07f30db849a267027077bf5abb994c6"
|
||||
"reference": "c87ba0a1da4cbf75d2cff9d3ea0262b78f1d86f6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/domains/zipball/0edf6bb2b07f30db849a267027077bf5abb994c6",
|
||||
"reference": "0edf6bb2b07f30db849a267027077bf5abb994c6",
|
||||
"url": "https://api.github.com/repos/utopia-php/domains/zipball/c87ba0a1da4cbf75d2cff9d3ea0262b78f1d86f6",
|
||||
"reference": "c87ba0a1da4cbf75d2cff9d3ea0262b78f1d86f6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4118,9 +4118,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/domains/issues",
|
||||
"source": "https://github.com/utopia-php/domains/tree/1.0.5"
|
||||
"source": "https://github.com/utopia-php/domains/tree/1.0.6"
|
||||
},
|
||||
"time": "2026-03-03T09:20:50+00:00"
|
||||
"time": "2026-04-29T11:08:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/dsn",
|
||||
@@ -4271,16 +4271,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/http",
|
||||
"version": "dev-fix/concurrency-shared-state",
|
||||
"version": "0.34.24",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/http.git",
|
||||
"reference": "e26b1a995f01b136ec88f6f2fd34811188b627cc"
|
||||
"reference": "d1eced0627c5a9fceddf53992ed97d664b810d33"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/e26b1a995f01b136ec88f6f2fd34811188b627cc",
|
||||
"reference": "e26b1a995f01b136ec88f6f2fd34811188b627cc",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/d1eced0627c5a9fceddf53992ed97d664b810d33",
|
||||
"reference": "d1eced0627c5a9fceddf53992ed97d664b810d33",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4321,9 +4321,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/http/issues",
|
||||
"source": "https://github.com/utopia-php/http/tree/fix/concurrency-shared-state"
|
||||
"source": "https://github.com/utopia-php/http/tree/0.34.24"
|
||||
},
|
||||
"time": "2026-04-28T15:24:50+00:00"
|
||||
"time": "2026-04-24T12:16:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/image",
|
||||
@@ -4530,16 +4530,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/migration",
|
||||
"version": "1.9.3",
|
||||
"version": "1.9.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/migration.git",
|
||||
"reference": "111f6221d04578a6f721c23ac872002375f176ae"
|
||||
"reference": "952a4dfe232702f80e45c35129466a8d8cb4c599"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/111f6221d04578a6f721c23ac872002375f176ae",
|
||||
"reference": "111f6221d04578a6f721c23ac872002375f176ae",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/952a4dfe232702f80e45c35129466a8d8cb4c599",
|
||||
"reference": "952a4dfe232702f80e45c35129466a8d8cb4c599",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4550,7 +4550,7 @@
|
||||
"php": ">=8.1",
|
||||
"utopia-php/database": "5.*",
|
||||
"utopia-php/dsn": "0.2.*",
|
||||
"utopia-php/storage": "1.0.*"
|
||||
"utopia-php/storage": "2.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-pdo": "*",
|
||||
@@ -4579,9 +4579,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/migration/issues",
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.9.3"
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.9.5"
|
||||
},
|
||||
"time": "2026-04-22T07:13:26+00:00"
|
||||
"time": "2026-04-29T11:19:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/mongo",
|
||||
@@ -5020,16 +5020,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/storage",
|
||||
"version": "1.0.1",
|
||||
"version": "2.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/storage.git",
|
||||
"reference": "f014be445f0baa635d0764e1673196f412511618"
|
||||
"reference": "8a2e3a86fd01aaed675884146665308c2122264e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/storage/zipball/f014be445f0baa635d0764e1673196f412511618",
|
||||
"reference": "f014be445f0baa635d0764e1673196f412511618",
|
||||
"url": "https://api.github.com/repos/utopia-php/storage/zipball/8a2e3a86fd01aaed675884146665308c2122264e",
|
||||
"reference": "8a2e3a86fd01aaed675884146665308c2122264e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5043,9 +5043,8 @@
|
||||
"utopia-php/validators": "0.2.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.2.*",
|
||||
"phpunit/phpunit": "^9.3",
|
||||
"vimeo/psalm": "4.0.1"
|
||||
"laravel/pint": "^1.21",
|
||||
"phpunit/phpunit": "^9.3"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
@@ -5067,9 +5066,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/storage/issues",
|
||||
"source": "https://github.com/utopia-php/storage/tree/1.0.1"
|
||||
"source": "https://github.com/utopia-php/storage/tree/2.0.1"
|
||||
},
|
||||
"time": "2026-02-23T05:59:32+00:00"
|
||||
"time": "2026-04-29T09:05:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/system",
|
||||
@@ -5466,16 +5465,16 @@
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "1.24.0",
|
||||
"version": "1.25.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "6d4d26659bc7a1c347c1d4d8dae3b77b5562e0cb"
|
||||
"reference": "f21a556b9acdbf75bbdcdc90a078af641646eade"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6d4d26659bc7a1c347c1d4d8dae3b77b5562e0cb",
|
||||
"reference": "6d4d26659bc7a1c347c1d4d8dae3b77b5562e0cb",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/f21a556b9acdbf75bbdcdc90a078af641646eade",
|
||||
"reference": "f21a556b9acdbf75bbdcdc90a078af641646eade",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5511,9 +5510,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.24.0"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.25.1"
|
||||
},
|
||||
"time": "2026-04-24T12:50:05+00:00"
|
||||
"time": "2026-04-28T11:12:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brianium/paratest",
|
||||
@@ -6222,11 +6221,11 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.1.51",
|
||||
"version": "2.1.54",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc3b523c45e714c70de2ac5113b958223b55dc59",
|
||||
"reference": "dc3b523c45e714c70de2ac5113b958223b55dc59",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
|
||||
"reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6271,7 +6270,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-21T18:22:01+00:00"
|
||||
"time": "2026-04-29T13:31:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
@@ -8443,18 +8442,9 @@
|
||||
"time": "2024-11-07T12:36:22+00:00"
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
{
|
||||
"package": "utopia-php/http",
|
||||
"version": "dev-fix/concurrency-shared-state",
|
||||
"alias": "0.34.99",
|
||||
"alias_normalized": "0.34.99.0"
|
||||
}
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "dev",
|
||||
"stability-flags": {
|
||||
"utopia-php/http": 20
|
||||
},
|
||||
"stability-flags": [],
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
@@ -8475,5 +8465,5 @@
|
||||
"platform-dev": {
|
||||
"ext-fileinfo": "*"
|
||||
},
|
||||
"plugin-api-version": "2.9.0"
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -806,6 +806,47 @@ services:
|
||||
- _APP_OPTIONS_FORCE_HTTPS
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
|
||||
appwrite-worker-notifications:
|
||||
entrypoint: worker-notifications
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-worker-notifications
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
depends_on:
|
||||
- redis
|
||||
- maildev
|
||||
- ${_APP_DB_HOST:-mariadb}
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_POOL_ADAPTER
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_SYSTEM_EMAIL_NAME
|
||||
- _APP_SYSTEM_EMAIL_ADDRESS
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_SMTP_HOST
|
||||
- _APP_SMTP_PORT
|
||||
- _APP_SMTP_SECURE
|
||||
- _APP_SMTP_USERNAME
|
||||
- _APP_SMTP_PASSWORD
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_DOMAIN
|
||||
- _APP_OPTIONS_FORCE_HTTPS
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
|
||||
appwrite-worker-messaging:
|
||||
entrypoint: worker-messaging
|
||||
<<: [*x-logging, *x-build]
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
List all OAuth2 providers supported by the Appwrite server, along with the parameters required to configure each provider. The response excludes mock providers but includes sandbox providers.
|
||||
@@ -1 +0,0 @@
|
||||
Get all Environment Variables that are relevant for the console.
|
||||
@@ -37,6 +37,7 @@
|
||||
<directory>./tests/e2e/Services/ProjectWebhooks</directory>
|
||||
<directory>./tests/e2e/Services/Messaging</directory>
|
||||
<directory>./tests/e2e/Services/Migrations</directory>
|
||||
<directory>./tests/e2e/Services/Notifications</directory>
|
||||
<directory>./tests/e2e/Services/Project</directory>
|
||||
<file>./tests/e2e/Services/Functions/FunctionsBase.php</file>
|
||||
<file>./tests/e2e/Services/Functions/FunctionsCustomServerTest.php</file>
|
||||
|
||||
@@ -105,7 +105,7 @@ class Key
|
||||
|
||||
/**
|
||||
* Decode the given secret key into a Key object, containing the project ID, type, role, scopes, and name.
|
||||
* Can be a stored API key or a dynamic key (JWT).
|
||||
* Can be a stored API key or an ephemeral key (JWT).
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
@@ -138,7 +138,9 @@ class Key
|
||||
);
|
||||
|
||||
switch ($type) {
|
||||
case API_KEY_DYNAMIC:
|
||||
// Dynamic supported for backwards compatibility
|
||||
case API_KEY_EPHEMERAL:
|
||||
case 'dynamic':
|
||||
$jwtObj = new JWT(
|
||||
key: System::getEnv('_APP_OPENSSL_KEY_V1'),
|
||||
algo: 'HS256',
|
||||
@@ -153,7 +155,7 @@ class Key
|
||||
$expired = true;
|
||||
}
|
||||
|
||||
$name = $payload['name'] ?? 'Dynamic Key';
|
||||
$name = $payload['name'] ?? 'Ephemeral Key';
|
||||
$projectId = $payload['projectId'] ?? '';
|
||||
$disabledMetrics = $payload['disabledMetrics'] ?? [];
|
||||
$hostnameOverride = $payload['hostnameOverride'] ?? false;
|
||||
|
||||
@@ -206,8 +206,6 @@ abstract class OAuth2
|
||||
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
\curl_close($ch);
|
||||
|
||||
if ($code >= 400) {
|
||||
throw new Exception($response, $code);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,13 @@ class Authentik extends OAuth2
|
||||
return 'authentik';
|
||||
}
|
||||
|
||||
public function verifyCredentials(): void
|
||||
{
|
||||
if (empty($this->getAuthentikDomain())) {
|
||||
throw new \Exception('Authentik endpoint is required.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -37,6 +37,13 @@ class FusionAuth extends OAuth2
|
||||
return 'fusionauth';
|
||||
}
|
||||
|
||||
public function verifyCredentials(): void
|
||||
{
|
||||
if (empty($this->getFusionAuthDomain())) {
|
||||
throw new \Exception('FusionAuth endpoint is required.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -37,6 +37,17 @@ class Keycloak extends OAuth2
|
||||
return 'keycloak';
|
||||
}
|
||||
|
||||
public function verifyCredentials(): void
|
||||
{
|
||||
if (empty($this->getKeycloakDomain())) {
|
||||
throw new \Exception('Keycloak endpoint is required.');
|
||||
}
|
||||
|
||||
if (empty($this->getKeycloakRealm())) {
|
||||
throw new \Exception('Keycloak realm name is required.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -36,6 +36,13 @@ class Microsoft extends OAuth2
|
||||
return 'microsoft';
|
||||
}
|
||||
|
||||
public function verifyCredentials(): void
|
||||
{
|
||||
if (empty($this->getTenantID())) {
|
||||
throw new \Exception('Microsoft tenant is required.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
@@ -201,7 +208,7 @@ class Microsoft extends OAuth2
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the Tenant Id from the JSON stored in appSecret. Defaults to 'common' as a fallback
|
||||
* Extracts the Tenant Id from the JSON stored in appSecret.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
@@ -209,6 +216,6 @@ class Microsoft extends OAuth2
|
||||
{
|
||||
$secret = $this->getAppSecret();
|
||||
|
||||
return $secret['tenantID'] ?? 'common';
|
||||
return $secret['tenantID'] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ class Event
|
||||
public const MAILS_QUEUE_NAME = 'v1-mails';
|
||||
public const MAILS_CLASS_NAME = 'MailsV1';
|
||||
|
||||
public const NOTIFICATIONS_QUEUE_NAME = 'v1-notifications';
|
||||
public const NOTIFICATIONS_CLASS_NAME = 'NotificationsV1';
|
||||
|
||||
public const FUNCTIONS_QUEUE_NAME = 'v1-functions';
|
||||
public const FUNCTIONS_CLASS_NAME = 'FunctionsV1';
|
||||
public const FUNCTIONS_QUEUE_TTL = 60 * 60 * 24 * 7; // 7 days
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event;
|
||||
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\System\System;
|
||||
|
||||
class Notification extends Event
|
||||
{
|
||||
protected string $recipient = '';
|
||||
protected string $name = '';
|
||||
protected string $subject = '';
|
||||
protected string $body = '';
|
||||
protected string $preview = '';
|
||||
protected array $smtp = [];
|
||||
protected array $variables = [];
|
||||
protected string $bodyTemplate = '';
|
||||
protected array $attachment = [];
|
||||
protected array $customMailOptions = [];
|
||||
|
||||
/**
|
||||
* Recipients to deliver the notification to.
|
||||
*
|
||||
* Each entry has an `address` (channel-specific identifier — email,
|
||||
* userId, or webhook URL) and a `channel`. Webhook recipients may
|
||||
* additionally carry an optional `signatureKey`; when set, the
|
||||
* webhook adapter signs the request body with HMAC-SHA256 and adds
|
||||
* the `X-Appwrite-Webhook-Signature` header. Without a key the
|
||||
* payload is sent unsigned.
|
||||
*
|
||||
* @var array<int, array{address: string, channel: string, signatureKey?: string}>
|
||||
*/
|
||||
protected array $recipients = [];
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected array $channels = [];
|
||||
|
||||
protected string $template = '';
|
||||
protected array $templateParams = [];
|
||||
protected string $deduplicationKey = '';
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected array $permissions = [];
|
||||
|
||||
public function __construct(protected Publisher $publisher)
|
||||
{
|
||||
parent::__construct($publisher);
|
||||
|
||||
$this
|
||||
->setQueue(System::getEnv('_APP_NOTIFICATIONS_QUEUE_NAME', Event::NOTIFICATIONS_QUEUE_NAME))
|
||||
->setClass(System::getEnv('_APP_NOTIFICATIONS_CLASS_NAME', Event::NOTIFICATIONS_CLASS_NAME));
|
||||
}
|
||||
|
||||
public function setSubject(string $subject): self
|
||||
{
|
||||
$this->subject = $subject;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSubject(): string
|
||||
{
|
||||
return $this->subject;
|
||||
}
|
||||
|
||||
public function setRecipient(string $recipient): self
|
||||
{
|
||||
$this->recipient = $recipient;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRecipient(): string
|
||||
{
|
||||
return $this->recipient;
|
||||
}
|
||||
|
||||
public function setBody(string $body): self
|
||||
{
|
||||
$this->body = $body;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBody(): string
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
public function setPreview(string $preview): self
|
||||
{
|
||||
$this->preview = $preview;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPreview(): string
|
||||
{
|
||||
return $this->preview;
|
||||
}
|
||||
|
||||
public function setName(string $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setBodyTemplate(string $bodyTemplate): self
|
||||
{
|
||||
$this->bodyTemplate = $bodyTemplate;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBodyTemplate(): string
|
||||
{
|
||||
return $this->bodyTemplate;
|
||||
}
|
||||
|
||||
public function setSmtpHost(string $host): self
|
||||
{
|
||||
$this->smtp['host'] = $host;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSmtpPort(int $port): self
|
||||
{
|
||||
$this->smtp['port'] = $port;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSmtpUsername(string $username): self
|
||||
{
|
||||
$this->smtp['username'] = $username;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSmtpPassword(string $password): self
|
||||
{
|
||||
$this->smtp['password'] = $password;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSmtpSecure(string $secure): self
|
||||
{
|
||||
$this->smtp['secure'] = $secure;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSmtpSenderEmail(string $senderEmail): self
|
||||
{
|
||||
$this->smtp['senderEmail'] = $senderEmail;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSmtpSenderName(string $senderName): self
|
||||
{
|
||||
$this->smtp['senderName'] = $senderName;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSmtpReplyToEmail(string $email): self
|
||||
{
|
||||
$this->smtp['replyToEmail'] = $email;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSmtpReplyToName(string $name): self
|
||||
{
|
||||
$this->smtp['replyToName'] = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSmtpHost(): string
|
||||
{
|
||||
return $this->smtp['host'] ?? '';
|
||||
}
|
||||
|
||||
public function getSmtpPort(): int
|
||||
{
|
||||
return $this->smtp['port'] ?? 0;
|
||||
}
|
||||
|
||||
public function getSmtpUsername(): string
|
||||
{
|
||||
return $this->smtp['username'] ?? '';
|
||||
}
|
||||
|
||||
public function getSmtpPassword(): string
|
||||
{
|
||||
return $this->smtp['password'] ?? '';
|
||||
}
|
||||
|
||||
public function getSmtpSecure(): string
|
||||
{
|
||||
return $this->smtp['secure'] ?? '';
|
||||
}
|
||||
|
||||
public function getSmtpSenderEmail(): string
|
||||
{
|
||||
return $this->smtp['senderEmail'] ?? '';
|
||||
}
|
||||
|
||||
public function getSmtpSenderName(): string
|
||||
{
|
||||
return $this->smtp['senderName'] ?? '';
|
||||
}
|
||||
|
||||
public function getSmtpReplyToEmail(): string
|
||||
{
|
||||
return $this->smtp['replyToEmail'] ?? '';
|
||||
}
|
||||
|
||||
public function getSmtpReplyToName(): string
|
||||
{
|
||||
return $this->smtp['replyToName'] ?? '';
|
||||
}
|
||||
|
||||
public function getVariables(): array
|
||||
{
|
||||
return $this->variables;
|
||||
}
|
||||
|
||||
public function setVariables(array $variables): self
|
||||
{
|
||||
$this->variables = $variables;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function appendVariables(array $variables): self
|
||||
{
|
||||
$this->variables = \array_merge($this->variables, $variables);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAttachment(string $content, string $filename, string $encoding = 'base64', string $type = 'plain/text'): self
|
||||
{
|
||||
$this->attachment = [
|
||||
'content' => \base64_encode($content),
|
||||
'filename' => $filename,
|
||||
'encoding' => $encoding,
|
||||
'type' => $type,
|
||||
];
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAttachment(): array
|
||||
{
|
||||
return $this->attachment;
|
||||
}
|
||||
|
||||
public function resetAttachment(): self
|
||||
{
|
||||
$this->attachment = [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSenderEmail(string $email): self
|
||||
{
|
||||
$this->customMailOptions['senderEmail'] = $email;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSenderEmail(): string
|
||||
{
|
||||
return $this->customMailOptions['senderEmail'] ?? '';
|
||||
}
|
||||
|
||||
public function setSenderName(string $name): self
|
||||
{
|
||||
$this->customMailOptions['senderName'] = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSenderName(): string
|
||||
{
|
||||
return $this->customMailOptions['senderName'] ?? '';
|
||||
}
|
||||
|
||||
public function setReplyToEmail(string $email): self
|
||||
{
|
||||
$this->customMailOptions['replyToEmail'] = $email;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getReplyToEmail(): string
|
||||
{
|
||||
return $this->customMailOptions['replyToEmail'] ?? '';
|
||||
}
|
||||
|
||||
public function setReplyToName(string $name): self
|
||||
{
|
||||
$this->customMailOptions['replyToName'] = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getReplyToName(): string
|
||||
{
|
||||
return $this->customMailOptions['replyToName'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{address: string, channel: string, signatureKey?: string}> $recipients
|
||||
*/
|
||||
public function setRecipients(array $recipients): self
|
||||
{
|
||||
$this->recipients = $recipients;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{address: string, channel: string, signatureKey?: string}>
|
||||
*/
|
||||
public function getRecipients(): array
|
||||
{
|
||||
return $this->recipients;
|
||||
}
|
||||
|
||||
public function addRecipient(string $address, string $channel = NOTIFICATION_TYPE_EMAIL, ?string $signatureKey = null): self
|
||||
{
|
||||
$recipient = ['address' => $address, 'channel' => $channel];
|
||||
if ($signatureKey !== null && $signatureKey !== '') {
|
||||
$recipient['signatureKey'] = $signatureKey;
|
||||
}
|
||||
$this->recipients[] = $recipient;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $channels
|
||||
*/
|
||||
public function setChannels(array $channels): self
|
||||
{
|
||||
$this->channels = $channels;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getChannels(): array
|
||||
{
|
||||
return $this->channels;
|
||||
}
|
||||
|
||||
public function setTemplate(string $template): self
|
||||
{
|
||||
$this->template = $template;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTemplate(): string
|
||||
{
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
public function setTemplateParams(array $params): self
|
||||
{
|
||||
$this->templateParams = $params;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTemplateParams(): array
|
||||
{
|
||||
return $this->templateParams;
|
||||
}
|
||||
|
||||
public function setDeduplicationKey(string $deduplicationKey): self
|
||||
{
|
||||
$this->deduplicationKey = $deduplicationKey;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDeduplicationKey(): string
|
||||
{
|
||||
return $this->deduplicationKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $permissions
|
||||
*/
|
||||
public function setPermissions(array $permissions): self
|
||||
{
|
||||
$this->permissions = $permissions;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getPermissions(): array
|
||||
{
|
||||
return $this->permissions;
|
||||
}
|
||||
|
||||
public function reset(): self
|
||||
{
|
||||
$this->project = null;
|
||||
$this->recipient = '';
|
||||
$this->name = '';
|
||||
$this->subject = '';
|
||||
$this->body = '';
|
||||
$this->variables = [];
|
||||
$this->bodyTemplate = '';
|
||||
$this->attachment = [];
|
||||
$this->customMailOptions = [];
|
||||
$this->recipients = [];
|
||||
$this->channels = [];
|
||||
$this->template = '';
|
||||
$this->templateParams = [];
|
||||
$this->deduplicationKey = '';
|
||||
$this->permissions = [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function preparePayload(): array
|
||||
{
|
||||
$platform = $this->platform;
|
||||
if (empty($platform)) {
|
||||
$platform = Config::getParam('platform', []);
|
||||
}
|
||||
|
||||
$recipients = $this->recipients;
|
||||
if (empty($recipients) && !empty($this->recipient)) {
|
||||
$recipients = [[
|
||||
'address' => $this->recipient,
|
||||
'channel' => NOTIFICATION_TYPE_EMAIL,
|
||||
]];
|
||||
}
|
||||
|
||||
return [
|
||||
'project' => $this->project,
|
||||
'recipient' => $this->recipient,
|
||||
'recipients' => $recipients,
|
||||
'channels' => $this->channels,
|
||||
'template' => $this->template,
|
||||
'templateParams' => $this->templateParams,
|
||||
'deduplicationKey' => $this->deduplicationKey,
|
||||
'permissions' => $this->permissions,
|
||||
'name' => $this->name,
|
||||
'subject' => $this->subject,
|
||||
'bodyTemplate' => $this->bodyTemplate,
|
||||
'body' => $this->body,
|
||||
'preview' => $this->preview,
|
||||
'smtp' => $this->smtp,
|
||||
'variables' => $this->variables,
|
||||
'attachment' => $this->attachment,
|
||||
'customMailOptions' => $this->customMailOptions,
|
||||
'events' => Event::generateEvents($this->getEvent(), $this->getParams()),
|
||||
'platform' => $platform,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -359,7 +359,7 @@ class Resolvers
|
||||
|
||||
$lock->acquire();
|
||||
|
||||
$original = $utopia->getResource('match');
|
||||
$original = $utopia->getRoute();
|
||||
try {
|
||||
$request = clone $request;
|
||||
|
||||
@@ -403,7 +403,7 @@ class Resolvers
|
||||
return;
|
||||
} finally {
|
||||
if ($original !== null) {
|
||||
$utopia->setContext('match', static fn () => $original);
|
||||
$utopia->setRoute($original);
|
||||
}
|
||||
|
||||
$lock->release();
|
||||
|
||||
@@ -14,6 +14,28 @@ use Utopia\Database\Query;
|
||||
|
||||
class Realtime extends MessagingAdapter
|
||||
{
|
||||
public const SUPPORTED_ACTIONS = ['create', 'update', 'upsert', 'delete'];
|
||||
|
||||
// Resources whose channels receive an action-suffixed sibling at publish time.
|
||||
// The suffix loop in fromPayload() treats any channel whose last OR second-to-last
|
||||
// segment matches an entry here as a candidate for `.{action}` suffixing.
|
||||
//
|
||||
// `functions` is intentionally a parent-only entry: fromPayload publishes
|
||||
// `functions.{functionId}` (suffixed to `functions.{functionId}.{action}`) but
|
||||
// never emits a bare `functions` channel — so subscribing to bare
|
||||
// `functions.{action}` is a silent no-op. Per-function filters
|
||||
// (`functions.{functionId}.{action}`) are the supported form.
|
||||
private const RESOURCE_LEAF_NAMES = [
|
||||
'documents',
|
||||
'rows',
|
||||
'files',
|
||||
'executions',
|
||||
'functions',
|
||||
'account',
|
||||
'teams',
|
||||
'memberships',
|
||||
];
|
||||
|
||||
/**
|
||||
* Connection Tree
|
||||
*
|
||||
@@ -45,8 +67,6 @@ class Realtime extends MessagingAdapter
|
||||
/**
|
||||
* Get the PubSubPool instance, initializing it lazily if needed.
|
||||
* This allows unit tests to work without requiring the global $register.
|
||||
*
|
||||
* @return PubSubPool
|
||||
*/
|
||||
private function getPubSubPool(): PubSubPool
|
||||
{
|
||||
@@ -54,6 +74,7 @@ class Realtime extends MessagingAdapter
|
||||
global $register;
|
||||
$this->pubSubPool = new PubSubPool($register->get('pools')->get('pubsub'));
|
||||
}
|
||||
|
||||
return $this->pubSubPool;
|
||||
}
|
||||
|
||||
@@ -147,7 +168,7 @@ class Realtime extends MessagingAdapter
|
||||
$roles = $this->connections[$connection]['roles'] ?? [];
|
||||
$channels = $this->connections[$connection]['channels'] ?? [];
|
||||
|
||||
if (!$projectId || empty($roles) || empty($channels)) {
|
||||
if (! $projectId || empty($roles) || empty($channels)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -168,7 +189,7 @@ class Realtime extends MessagingAdapter
|
||||
if (!isset($subscriptions[$subscriptionId])) {
|
||||
$subscriptions[$subscriptionId] = [
|
||||
'channels' => [],
|
||||
'queries' => $data['strings'] ?? []
|
||||
'queries' => $data['strings'] ?? [],
|
||||
];
|
||||
}
|
||||
if (!\in_array($channel, $subscriptions[$subscriptionId]['channels'])) {
|
||||
@@ -306,7 +327,7 @@ class Realtime extends MessagingAdapter
|
||||
*/
|
||||
public function hasSubscriber(string $projectId, string $role, string $channel = ''): bool
|
||||
{
|
||||
//TODO: look into moving it to an abstract class in the parent class
|
||||
// TODO: look into moving it to an abstract class in the parent class
|
||||
if (empty($channel)) {
|
||||
return array_key_exists($projectId, $this->subscriptions)
|
||||
&& array_key_exists($role, $this->subscriptions[$projectId]);
|
||||
@@ -327,6 +348,7 @@ class Realtime extends MessagingAdapter
|
||||
* @param array $roles
|
||||
* @param array $options
|
||||
* @return void
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function send(string $projectId, array $payload, array $events, array $channels, array $roles, array $options = []): void
|
||||
@@ -347,8 +369,8 @@ class Realtime extends MessagingAdapter
|
||||
'events' => $events,
|
||||
'channels' => $channels,
|
||||
'timestamp' => DateTime::formatTz(DateTime::now()),
|
||||
'payload' => $payload
|
||||
]
|
||||
'payload' => $payload,
|
||||
],
|
||||
]));
|
||||
}
|
||||
|
||||
@@ -361,7 +383,6 @@ class Realtime extends MessagingAdapter
|
||||
* - 1.5 ms | 1,000 Connections / 10,000 Subscriptions
|
||||
* - 15 ms | 10,000 Connections / 100,000 Subscriptions
|
||||
*
|
||||
* @param array $event
|
||||
* @return array<int|string, array> Map of connection IDs to matched query groups
|
||||
*/
|
||||
public function getSubscribers(array $event): array
|
||||
@@ -377,7 +398,7 @@ class Realtime extends MessagingAdapter
|
||||
foreach ($this->subscriptions[$event['project']] as $role => $subscriptionsByChannel) {
|
||||
foreach ($event['data']['channels'] as $channel) {
|
||||
if (
|
||||
!\array_key_exists($channel, $subscriptionsByChannel)
|
||||
! \array_key_exists($channel, $subscriptionsByChannel)
|
||||
|| (!\in_array($role, $event['roles']) && !\in_array(Role::any()->toString(), $event['roles']))
|
||||
) {
|
||||
continue;
|
||||
@@ -410,6 +431,12 @@ class Realtime extends MessagingAdapter
|
||||
|
||||
/**
|
||||
* Converts the channels from the Query Params into an array.
|
||||
* Also renames the account channel to account.USER_ID, rewrites action-suffixed
|
||||
* account variants (`account.create`, `account.update`, `account.upsert`,
|
||||
* `account.delete`) to `account.USER_ID.{action}` so they match the channels
|
||||
* fromPayload() publishes for top-level user events, and removes all other
|
||||
* illegal account channel variations (e.g. another user's `account.{otherId}`).
|
||||
*
|
||||
* Also renames the account channel to account.USER_ID and removes all illegal account channel variations.
|
||||
* @param array $channels
|
||||
* @param string $userId
|
||||
@@ -421,27 +448,94 @@ class Realtime extends MessagingAdapter
|
||||
|
||||
foreach ($channels as $key => $value) {
|
||||
switch (true) {
|
||||
case str_starts_with($key, 'account.'):
|
||||
unset($channels[$key]);
|
||||
break;
|
||||
|
||||
case $key === 'account':
|
||||
if (!empty($userId)) {
|
||||
$channels['account.' . $userId] = $value;
|
||||
$channels['account.'.$userId] = $value;
|
||||
}
|
||||
break;
|
||||
|
||||
case \in_array(\substr($key, \strlen('account.')), self::SUPPORTED_ACTIONS, true) && str_starts_with($key, 'account.'):
|
||||
// Authenticated: rewrite `account.{action}` → `account.{userId}.{action}`
|
||||
// so the subscriber only receives their own account events.
|
||||
// Guest: keep the literal `account.{action}` so the action filter
|
||||
// applies to the broadcast `account.{action}` channel that fromPayload
|
||||
// emits for top-level user events. On in-band auth, rebindAccountChannels
|
||||
// rewrites the literal to the user-scoped form.
|
||||
if (!empty($userId)) {
|
||||
unset($channels[$key]);
|
||||
$action = \substr($key, \strlen('account.'));
|
||||
$channels['account.'.$userId.'.'.$action] = $value;
|
||||
}
|
||||
break;
|
||||
|
||||
case str_starts_with($key, 'account.'):
|
||||
unset($channels[$key]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites stored account channels to match a new userId. Used when in-band
|
||||
* authentication changes the connection's user identity:
|
||||
*
|
||||
* - guest → authenticated: rewrites the literal `account.{action}` form
|
||||
* that convertChannels preserves for guests into `account.{userId}.{action}`.
|
||||
* - reauth as a different user: rewrites `account.{oldUserId}` and
|
||||
* `account.{oldUserId}.{action}` to the new userId.
|
||||
*
|
||||
* Returns channels unchanged when there's nothing to do — same user, or an
|
||||
* empty target (defensive: avoids producing malformed `account.` strings if
|
||||
* a caller ever passes `$newUserId = ''`, e.g. an in-band logout flow).
|
||||
*/
|
||||
public static function rebindAccountChannels(array $channels, string $oldUserId, string $newUserId): array
|
||||
{
|
||||
if ($newUserId === '' || $oldUserId === $newUserId) {
|
||||
return $channels;
|
||||
}
|
||||
|
||||
return \array_map(function (string $channel) use ($oldUserId, $newUserId) {
|
||||
if (!\str_starts_with($channel, 'account.')) {
|
||||
return $channel;
|
||||
}
|
||||
|
||||
// Guest origin: literal `account.{action}` (preserved by convertChannels
|
||||
// for unauthenticated connections) becomes `account.{newUserId}.{action}`.
|
||||
if ($oldUserId === '') {
|
||||
$suffix = \substr($channel, \strlen('account.'));
|
||||
if (\in_array($suffix, self::SUPPORTED_ACTIONS, true)) {
|
||||
return 'account.'.$newUserId.'.'.$suffix;
|
||||
}
|
||||
|
||||
return $channel;
|
||||
}
|
||||
|
||||
// Authenticated → different user.
|
||||
if ($channel === 'account.'.$oldUserId) {
|
||||
return 'account.'.$newUserId;
|
||||
}
|
||||
|
||||
$oldPrefix = 'account.'.$oldUserId.'.';
|
||||
if (\str_starts_with($channel, $oldPrefix)) {
|
||||
$action = \substr($channel, \strlen($oldPrefix));
|
||||
if (\in_array($action, self::SUPPORTED_ACTIONS, true)) {
|
||||
return 'account.'.$newUserId.'.'.$action;
|
||||
}
|
||||
}
|
||||
|
||||
return $channel;
|
||||
}, $channels);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs subscriptions from query parameters.
|
||||
*
|
||||
* @param array $channelNames
|
||||
* @param callable $getQueryParam
|
||||
* @return array [index => ['channels' => string[], 'queries' => Query[]]]
|
||||
*
|
||||
* @throws QueryException
|
||||
*/
|
||||
public static function constructSubscriptions(array $channelNames, callable $getQueryParam): array
|
||||
@@ -482,10 +576,11 @@ class Realtime extends MessagingAdapter
|
||||
if (empty($subscriptions[0]['queries'])) {
|
||||
$subscriptions[0]['queries'] = [Query::select(['*'])];
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!\is_array($params)) {
|
||||
if (! \is_array($params)) {
|
||||
$params = [$params];
|
||||
}
|
||||
|
||||
@@ -512,6 +607,7 @@ class Realtime extends MessagingAdapter
|
||||
* Converts the queries from the Query Params into an array.
|
||||
* @param array|string $queries
|
||||
* @return array
|
||||
*
|
||||
* @throws QueryException
|
||||
*/
|
||||
public static function convertQueries(mixed $queries): array
|
||||
@@ -524,7 +620,7 @@ class Realtime extends MessagingAdapter
|
||||
$query = array_pop($stack);
|
||||
$method = $query->getMethod();
|
||||
|
||||
if (!in_array($method, RuntimeQuery::ALLOWED_QUERIES, true)) {
|
||||
if (! in_array($method, RuntimeQuery::ALLOWED_QUERIES, true)) {
|
||||
throw new QueryException(
|
||||
"Query method '{$method}' is not supported in Realtime queries. Allowed: {$allowed}"
|
||||
);
|
||||
@@ -680,11 +776,53 @@ class Realtime extends MessagingAdapter
|
||||
break;
|
||||
}
|
||||
|
||||
// Action is the last segment for plain CRUD events (e.g. `documents.X.create`),
|
||||
// and the second-to-last segment for attribute-trailing events
|
||||
// (e.g. `users.U.update.email`, `teams.T.update.prefs`,
|
||||
// `teams.T.memberships.M.update.status`). Without the second-to-last fallback
|
||||
$count = \count($parts);
|
||||
$action = null;
|
||||
if (\in_array($parts[$count - 1], self::SUPPORTED_ACTIONS, true)) {
|
||||
$action = $parts[$count - 1];
|
||||
} elseif ($count >= 2 && \in_array($parts[$count - 2], self::SUPPORTED_ACTIONS, true)) {
|
||||
$action = $parts[$count - 2];
|
||||
}
|
||||
|
||||
// The `users` branch emits only user-level account channels
|
||||
// (`account`, `account.{userId}`) regardless of event depth, so nested events
|
||||
// like `users.U.sessions.S.create` or `users.U.challenges.C.create` would
|
||||
// otherwise be suffixed as `account.create` — making a subscription to
|
||||
// `account.create` receive unrelated session/challenge/recovery/verification
|
||||
// events. Restrict suffixing to top-level user events where the action sits
|
||||
// at parts[2] (`users.U.create`, `users.U.update.email`, etc.).
|
||||
if (
|
||||
$action !== null
|
||||
&& $parts[0] === 'users'
|
||||
&& ($parts[2] ?? null) !== $action
|
||||
) {
|
||||
$action = null;
|
||||
}
|
||||
|
||||
if ($action !== null && !empty($channels)) {
|
||||
$augmented = $channels;
|
||||
foreach ($channels as $channel) {
|
||||
$segments = \explode('.', $channel);
|
||||
$segCount = \count($segments);
|
||||
$leafIsResource = \in_array($segments[$segCount - 1], self::RESOURCE_LEAF_NAMES, true);
|
||||
$parentIsResource = $segCount >= 2 && \in_array($segments[$segCount - 2], self::RESOURCE_LEAF_NAMES, true);
|
||||
|
||||
if ($leafIsResource || $parentIsResource) {
|
||||
$augmented[] = $channel. '.' .$action;
|
||||
}
|
||||
}
|
||||
$channels = \array_values(\array_unique($augmented));
|
||||
}
|
||||
|
||||
return [
|
||||
'channels' => $channels,
|
||||
'roles' => $roles,
|
||||
'permissionsChanged' => $permissionsChanged,
|
||||
'projectId' => $projectId
|
||||
'projectId' => $projectId,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ abstract class Migration
|
||||
'1.9.0' => 'V24',
|
||||
'1.9.1' => 'V24',
|
||||
'1.9.2' => 'V24',
|
||||
'1.9.3' => 'V24',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,7 @@ use Appwrite\Platform\Modules\Core;
|
||||
use Appwrite\Platform\Modules\Databases;
|
||||
use Appwrite\Platform\Modules\Functions;
|
||||
use Appwrite\Platform\Modules\Health;
|
||||
use Appwrite\Platform\Modules\Migrations;
|
||||
use Appwrite\Platform\Modules\Project;
|
||||
use Appwrite\Platform\Modules\Projects;
|
||||
use Appwrite\Platform\Modules\Proxy;
|
||||
@@ -39,6 +40,7 @@ class Appwrite extends Platform
|
||||
$this->addModule(new Storage\Module());
|
||||
$this->addModule(new VCS\Module());
|
||||
$this->addModule(new Webhooks\Module());
|
||||
$this->addModule(new Migrations\Module());
|
||||
$this->addModule(new Project\Module());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ class Get extends Action
|
||||
curl_setopt_array($ch, $options);
|
||||
curl_exec($ch);
|
||||
$errno = curl_errno($ch);
|
||||
curl_close($ch);
|
||||
|
||||
return $errno === 0;
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ class Server
|
||||
|
||||
$nativeServer = $adapter->getNativeServer();
|
||||
|
||||
$container = $adapter->getContext();
|
||||
$container = $adapter->getContainer();
|
||||
$container->set('installerState', fn () => $state);
|
||||
$container->set('installerConfig', fn () => $config);
|
||||
$container->set('installerPaths', fn () => $paths);
|
||||
|
||||
@@ -85,8 +85,6 @@ class Create extends Action
|
||||
|
||||
curl_exec($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
$response->chunk('', true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class XList extends Action
|
||||
namespace: 'console',
|
||||
group: 'console',
|
||||
name: 'listOAuth2Providers',
|
||||
description: '/docs/references/console/list-oauth2-providers.md',
|
||||
description: 'List all OAuth2 providers supported by the Appwrite server, along with the parameters required to configure each provider. The response excludes mock providers but includes sandbox providers.',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
@@ -54,9 +54,9 @@ class XList extends Action
|
||||
$actions = OAuth2Base::getProviderActions();
|
||||
|
||||
$providers = [];
|
||||
foreach ($actions as $providerId => $updateClass) {
|
||||
$config = $providersConfig[$providerId] ?? null;
|
||||
if ($config === null) {
|
||||
foreach ($providersConfig as $providerId => $config) {
|
||||
$updateClass = $actions[$providerId] ?? null;
|
||||
if ($updateClass === null) {
|
||||
continue;
|
||||
}
|
||||
if (!($config['enabled'] ?? false)) {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Console\Http\Scopes\Key;
|
||||
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class XList extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'listConsoleProjectScopes';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/console/scopes/project')
|
||||
->desc('List project scopes')
|
||||
->groups(['api'])
|
||||
->label('scope', 'public')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'console',
|
||||
group: 'console',
|
||||
name: 'listProjectScopes',
|
||||
description: 'List all scopes available for project API keys, along with a description for each scope.',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_CONSOLE_KEY_SCOPE_LIST,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->inject('response')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(Response $response): void
|
||||
{
|
||||
$scopesConfig = Config::getParam('projectScopes', []);
|
||||
|
||||
$scopes = [];
|
||||
foreach ($scopesConfig as $scopeId => $scope) {
|
||||
$scopes[] = new Document([
|
||||
'$id' => $scopeId,
|
||||
'description' => $scope['description'] ?? '',
|
||||
'category' => $scope['category'] ?? '',
|
||||
'deprecated' => $scope['deprecated'] ?? false,
|
||||
]);
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'total' => \count($scopes),
|
||||
'scopes' => $scopes,
|
||||
]), Response::MODEL_CONSOLE_KEY_SCOPE_LIST);
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ class Get extends Action
|
||||
namespace: 'console',
|
||||
group: 'console',
|
||||
name: 'variables',
|
||||
description: '/docs/references/console/variables.md',
|
||||
description: 'Get all Environment Variables that are relevant for the console.',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
|
||||
@@ -15,6 +15,7 @@ use Appwrite\Platform\Modules\Console\Http\Redirects\Recover\Get as RedirectReco
|
||||
use Appwrite\Platform\Modules\Console\Http\Redirects\Register\Get as RedirectRegister;
|
||||
use Appwrite\Platform\Modules\Console\Http\Redirects\Root\Get as RedirectRoot;
|
||||
use Appwrite\Platform\Modules\Console\Http\Resources\Get as GetResourceAvailability;
|
||||
use Appwrite\Platform\Modules\Console\Http\Scopes\Key\XList as ListKeyScopes;
|
||||
use Appwrite\Platform\Modules\Console\Http\Variables\Get as GetVariables;
|
||||
use Utopia\Platform\Service;
|
||||
|
||||
@@ -30,6 +31,7 @@ class Http extends Service
|
||||
|
||||
$this->addAction(GetVariables::getName(), new GetVariables());
|
||||
$this->addAction(ListOAuth2Providers::getName(), new ListOAuth2Providers());
|
||||
$this->addAction(ListKeyScopes::getName(), new ListKeyScopes());
|
||||
$this->addAction(CreateAssistantQuery::getName(), new CreateAssistantQuery());
|
||||
$this->addAction(GetResourceAvailability::getName(), new GetResourceAvailability());
|
||||
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ class Create extends BooleanCreate
|
||||
->desc('Create boolean column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('audits.event', 'column.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ class Update extends BooleanUpdate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/boolean/:key')
|
||||
->desc('Update boolean column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
|
||||
->label('audits.event', 'column.update')
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ class Create extends DatetimeCreate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/datetime')
|
||||
->desc('Create datetime column')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
|
||||
->label('audits.event', 'column.create')
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ class Update extends DatetimeUpdate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/datetime/:key')
|
||||
->desc('Update dateTime column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
|
||||
->label('audits.event', 'column.update')
|
||||
|
||||
@@ -33,7 +33,7 @@ class Delete extends AttributesDelete
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/:key')
|
||||
->desc('Delete column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
|
||||
->label('audits.event', 'column.delete')
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ class Create extends EmailCreate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/email')
|
||||
->desc('Create email column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
|
||||
->label('audits.event', 'column.create')
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ class Update extends EmailUpdate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/email/:key')
|
||||
->desc('Update email column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
|
||||
->label('audits.event', 'column.update')
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ class Create extends EnumCreate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/enum')
|
||||
->desc('Create enum column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
|
||||
->label('audits.event', 'column.create')
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ class Update extends EnumUpdate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/enum/:key')
|
||||
->desc('Update enum column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
|
||||
->label('audits.event', 'column.update')
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ class Create extends FloatCreate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/float')
|
||||
->desc('Create float column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
|
||||
->label('audits.event', 'column.create')
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ class Update extends FloatUpdate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/float/:key')
|
||||
->desc('Update float column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
|
||||
->label('audits.event', 'column.update')
|
||||
|
||||
@@ -42,7 +42,7 @@ class Get extends AttributesGet
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/:key')
|
||||
->desc('Get column')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', ['tables.read', 'collections.read'])
|
||||
->label('scope', ['tables.read', 'collections.read', 'columns.read', 'attributes.read'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSDKNamespace(),
|
||||
|
||||
@@ -34,7 +34,7 @@ class Create extends IPCreate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/ip')
|
||||
->desc('Create IP address column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
|
||||
->label('audits.event', 'column.create')
|
||||
|
||||
@@ -35,7 +35,7 @@ class Update extends IPUpdate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/ip/:key')
|
||||
->desc('Update IP address column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
|
||||
->label('audits.event', 'column.update')
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ class Create extends IntegerCreate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/integer')
|
||||
->desc('Create integer column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
|
||||
->label('audits.event', 'column.create')
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ class Update extends IntegerUpdate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/integer/:key')
|
||||
->desc('Update integer column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
|
||||
->label('audits.event', 'column.update')
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ class Create extends LineCreate
|
||||
->desc('Create line column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('audits.event', 'column.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ class Update extends LineUpdate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/line/:key')
|
||||
->desc('Update line column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
|
||||
->label('audits.event', 'column.update')
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ class Create extends LongtextCreate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/longtext')
|
||||
->desc('Create longtext column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
|
||||
->label('audits.event', 'column.create')
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ class Update extends LongtextUpdate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/longtext/:key')
|
||||
->desc('Update longtext column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
|
||||
->label('audits.event', 'column.update')
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ class Create extends MediumtextCreate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/mediumtext')
|
||||
->desc('Create mediumtext column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
|
||||
->label('audits.event', 'column.create')
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ class Update extends MediumtextUpdate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/mediumtext/:key')
|
||||
->desc('Update mediumtext column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
|
||||
->label('audits.event', 'column.update')
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ class Create extends PointCreate
|
||||
->desc('Create point column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('audits.event', 'column.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ class Update extends PointUpdate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/point/:key')
|
||||
->desc('Update point column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
|
||||
->label('audits.event', 'column.update')
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ class Create extends PolygonCreate
|
||||
->desc('Create polygon column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('audits.event', 'column.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ class Update extends PolygonUpdate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/polygon/:key')
|
||||
->desc('Update polygon column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
|
||||
->label('audits.event', 'column.update')
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ class Create extends RelationshipCreate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/relationship')
|
||||
->desc('Create relationship column')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
|
||||
->label('audits.event', 'column.create')
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ class Update extends RelationshipUpdate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/:key/relationship')
|
||||
->desc('Update relationship column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
|
||||
->label('audits.event', 'column.update')
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ class Create extends StringCreate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/string')
|
||||
->desc('Create string column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
|
||||
->label('audits.event', 'column.create')
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ class Update extends StringUpdate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/string/:key')
|
||||
->desc('Update string column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
|
||||
->label('audits.event', 'column.update')
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ class Create extends TextCreate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/text')
|
||||
->desc('Create text column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
|
||||
->label('audits.event', 'column.create')
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ class Update extends TextUpdate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/text/:key')
|
||||
->desc('Update text column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
|
||||
->label('audits.event', 'column.update')
|
||||
|
||||
@@ -34,7 +34,7 @@ class Create extends URLCreate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/url')
|
||||
->desc('Create URL column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
|
||||
->label('audits.event', 'column.create')
|
||||
|
||||
@@ -35,7 +35,7 @@ class Update extends URLUpdate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/url/:key')
|
||||
->desc('Update URL column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
|
||||
->label('audits.event', 'column.update')
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ class Create extends VarcharCreate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/varchar')
|
||||
->desc('Create varchar column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
|
||||
->label('audits.event', 'column.create')
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ class Update extends VarcharUpdate
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/varchar/:key')
|
||||
->desc('Update varchar column')
|
||||
->groups(['api', 'database', 'schema'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
|
||||
->label('audits.event', 'column.update')
|
||||
|
||||
@@ -33,7 +33,7 @@ class XList extends AttributesXList
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns')
|
||||
->desc('List columns')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', ['tables.read', 'collections.read'])
|
||||
->label('scope', ['tables.read', 'collections.read', 'columns.read', 'attributes.read'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSDKNamespace(),
|
||||
|
||||
@@ -37,7 +37,7 @@ class Create extends IndexCreate
|
||||
->desc('Create index')
|
||||
->groups(['api', 'database'])
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].indexes.[indexId].create')
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'indexes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('audits.event', 'index.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
|
||||
|
||||
@@ -36,7 +36,7 @@ class Delete extends IndexDelete
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/indexes/:key')
|
||||
->desc('Delete index')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', ['tables.write', 'collections.write'])
|
||||
->label('scope', ['tables.write', 'collections.write', 'indexes.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('event', 'databases.[databaseId].tables.[tableId].indexes.[indexId].update')
|
||||
->label('audits.event', 'index.delete')
|
||||
|
||||
@@ -32,7 +32,7 @@ class Get extends IndexGet
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/indexes/:key')
|
||||
->desc('Get index')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', ['tables.read', 'collections.read'])
|
||||
->label('scope', ['tables.read', 'collections.read', 'indexes.read'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSDKNamespace(),
|
||||
|
||||
@@ -33,7 +33,7 @@ class XList extends IndexXList
|
||||
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/indexes')
|
||||
->desc('List indexes')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', ['tables.read', 'collections.read'])
|
||||
->label('scope', ['tables.read', 'collections.read', 'indexes.read'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: $this->getSDKNamespace(),
|
||||
|
||||
@@ -175,15 +175,8 @@ class Create extends Action
|
||||
throw new Exception(Exception::STORAGE_INVALID_CONTENT_RANGE);
|
||||
}
|
||||
|
||||
// TODO remove the condition that checks `$end === $fileSize` in next breaking version
|
||||
if ($end === $fileSize - 1 || $end === $fileSize) {
|
||||
//if it's a last chunks the chunk size might differ, so we set the $chunks and $chunk to notify it's last chunk
|
||||
$chunks = $chunk = -1;
|
||||
} else {
|
||||
// Calculate total number of chunks based on the chunk size i.e ($rangeEnd - $rangeStart)
|
||||
$chunks = (int) ceil($fileSize / ($end + 1 - $start));
|
||||
$chunk = (int) ($start / ($end + 1 - $start)) + 1;
|
||||
}
|
||||
$chunks = (int) ceil($fileSize / APP_LIMIT_UPLOAD_CHUNK_SIZE);
|
||||
$chunk = (int) ($start / APP_LIMIT_UPLOAD_CHUNK_SIZE) + 1;
|
||||
}
|
||||
|
||||
if (!$fileSizeValidator->isValid($fileSize) && $functionSizeLimit !== 0) { // Check if file size is exceeding allowed limit
|
||||
@@ -202,15 +195,14 @@ class Create extends Action
|
||||
$metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)];
|
||||
if (!$deployment->isEmpty()) {
|
||||
$chunks = $deployment->getAttribute('sourceChunksTotal', 1);
|
||||
$uploaded = $deployment->getAttribute('sourceChunksUploaded', 0);
|
||||
$metadata = $deployment->getAttribute('sourceMetadata', []);
|
||||
if ($chunk === -1) {
|
||||
$chunk = $chunks;
|
||||
}
|
||||
} else {
|
||||
// Guard against manually setting range header for single chunk upload
|
||||
if ($chunks === -1) {
|
||||
$chunks = 1;
|
||||
$chunk = 1;
|
||||
|
||||
if ($uploaded === $chunks) {
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,6 +250,8 @@ class Create extends Action
|
||||
'sourcePath' => $path,
|
||||
'sourceSize' => $fileSize,
|
||||
'totalSize' => $fileSize,
|
||||
'sourceChunksTotal' => $chunks,
|
||||
'sourceChunksUploaded' => $chunksUploaded,
|
||||
'activate' => $activate,
|
||||
'sourceMetadata' => $metadata,
|
||||
'type' => $type
|
||||
@@ -272,6 +266,7 @@ class Create extends Action
|
||||
} else {
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
|
||||
'sourceSize' => $fileSize,
|
||||
'sourceChunksUploaded' => $chunksUploaded,
|
||||
'sourceMetadata' => $metadata,
|
||||
]));
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class Create extends Base
|
||||
->setHttpPath('/v1/functions/:functionId/executions')
|
||||
->desc('Create execution')
|
||||
->groups(['api', 'functions'])
|
||||
->label('scope', 'execution.write')
|
||||
->label('scope', ['executions.write', 'execution.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
|
||||
->label('event', 'functions.[functionId].executions.[executionId].create')
|
||||
->label('sdk', new Method(
|
||||
@@ -228,7 +228,7 @@ class Create extends Base
|
||||
|
||||
$executionId = ID::unique();
|
||||
$headers['x-appwrite-execution-id'] = $executionId;
|
||||
$headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey;
|
||||
$headers['x-appwrite-key'] = API_KEY_EPHEMERAL . '_' . $apiKey;
|
||||
$headers['x-appwrite-trigger'] = 'http';
|
||||
$headers['x-appwrite-user-id'] = $user->getId();
|
||||
$headers['x-appwrite-user-jwt'] = $jwt;
|
||||
|
||||
@@ -35,7 +35,7 @@ class Delete extends Base
|
||||
->setHttpPath('/v1/functions/:functionId/executions/:executionId')
|
||||
->desc('Delete execution')
|
||||
->groups(['api', 'functions'])
|
||||
->label('scope', 'execution.write')
|
||||
->label('scope', ['executions.write', 'execution.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
|
||||
->label('event', 'functions.[functionId].executions.[executionId].delete')
|
||||
->label('audits.event', 'executions.delete')
|
||||
|
||||
@@ -31,7 +31,7 @@ class Get extends Base
|
||||
->setHttpPath('/v1/functions/:functionId/executions/:executionId')
|
||||
->desc('Get execution')
|
||||
->groups(['api', 'functions'])
|
||||
->label('scope', 'execution.read')
|
||||
->label('scope', ['executions.read', 'execution.read'])
|
||||
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'functions',
|
||||
|
||||
@@ -39,7 +39,7 @@ class XList extends Base
|
||||
->setHttpPath('/v1/functions/:functionId/executions')
|
||||
->desc('List executions')
|
||||
->groups(['api', 'functions'])
|
||||
->label('scope', 'execution.read')
|
||||
->label('scope', ['executions.read', 'execution.read'])
|
||||
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'functions',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user