Merge remote-tracking branch 'origin/1.6.x' into feat-docker-geo

This commit is contained in:
Damodar Lohani
2025-01-07 02:59:27 +00:00
88 changed files with 4592 additions and 3452 deletions
+3 -1
View File
@@ -23,6 +23,7 @@ _APP_OPENSSL_KEY_V1=your-secret-key
_APP_DOMAIN=traefik
_APP_DOMAIN_FUNCTIONS=functions.localhost
_APP_DOMAIN_TARGET=localhost
_APP_RULES_FORMAT=md5
_APP_REDIS_HOST=redis
_APP_REDIS_PORT=6379
_APP_REDIS_PASS=
@@ -108,4 +109,5 @@ _APP_MESSAGE_EMAIL_TEST_DSN=
_APP_MESSAGE_PUSH_TEST_DSN=
_APP_WEBHOOK_MAX_FAILED_ATTEMPTS=10
_APP_PROJECT_REGIONS=default
_APP_GEO_SECRET=geo-secret-key
_APP_GEO_SECRET=geo-secret-key
_APP_FUNCTIONS_CREATION_ABUSE_LIMIT=5000
+33
View File
@@ -0,0 +1,33 @@
name: "Console SDK Preview"
on:
pull_request:
paths:
- 'app/config/specs/*-latest-console.json'
jobs:
setup:
name: Setup & Build Console SDK
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Load and Start Appwrite
run: |
docker compose build
docker compose up -d
docker compose exec appwrite sdks --platform=console --sdk=web --version=latest --git=no
sudo chown -R $USER:$USER ./app/sdks/console-web
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Build and Publish SDK
working-directory: ./app/sdks/console-web
run: |
npm install
npm run build
npx pkg-pr-new publish --comment=update
+25 -5
View File
@@ -127,6 +127,11 @@ jobs:
Messaging,
Migrations
]
tables-mode: [
'Project',
'Shared V1',
'Shared V2',
]
steps:
- name: checkout
@@ -145,11 +150,26 @@ jobs:
docker compose up -d
sleep 30
- name: Run ${{matrix.service}} Tests
run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug
- name: Run ${{matrix.service}} Shared Tables Tests
run: _APP_DATABASE_SHARED_TABLES=database_db_main docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug
- name: Run ${{ matrix.service }} tests with ${{ matrix.tables-mode }} table mode
run: |
if [ "${{ matrix.tables-mode }}" == "Shared V1" ]; then
echo "Using shared tables V1"
export _APP_DATABASE_SHARED_TABLES=database_db_main
export _APP_DATABASE_SHARED_TABLES_V1=database_db_main
elif [ "${{ matrix.tables-mode }}" == "Shared V2" ]; then
echo "Using shared tables V2"
export _APP_DATABASE_SHARED_TABLES=database_db_main
export _APP_DATABASE_SHARED_TABLES_V1=
else
echo "Using project tables"
export _APP_DATABASE_SHARED_TABLES=
export _APP_DATABASE_SHARED_TABLES_V1=
fi
docker compose exec -T \
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug
benchmarking:
name: Benchmark
+1
View File
@@ -16,3 +16,4 @@ dev/yasd_init.php
.phpunit.result.cache
Makefile
appwrite.json
/.zed/
+114
View File
@@ -1,3 +1,117 @@
# Version 1.6.1
## What's Changed
### Notable changes
* Remove JPEG fallback for webp by @lohanidamodar in https://github.com/appwrite/appwrite/pull/8746
* Add heic and avif support by @lohanidamodar in https://github.com/appwrite/appwrite/pull/7718
* Add new runtimes by @Meldiron in https://github.com/appwrite/appwrite/pull/8771
* Remove audits deletion by @shimonewman in https://github.com/appwrite/appwrite/pull/8766
* Bump assistant by @loks0n in https://github.com/appwrite/appwrite/pull/8801
* Change max queries values to 500 by @fogelito in https://github.com/appwrite/appwrite/pull/8802
* Allow '.wav' as 'audio/x-wav' as well by @basert in https://github.com/appwrite/appwrite/pull/8846
* Use 1 instead of 0.5 cpu for default function specification by @loks0n in https://github.com/appwrite/appwrite/pull/8848
* Update function runtimes by @christyjacob4 in https://github.com/appwrite/appwrite/pull/8781
* Add a realtime heartbeat by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/8943
### Fixes
* Trigger functions event only if event is not paused by @lohanidamodar in https://github.com/appwrite/appwrite/pull/8526
* Update docker-compose to restart usage-dump by @feschaffa in https://github.com/appwrite/appwrite/pull/8642
* Fix typo in scheduler base by @fogelito in https://github.com/appwrite/appwrite/pull/8691
* Add domain and force HTTPS env vars to mail worker by @stnguyen90 in https://github.com/appwrite/appwrite/pull/8722
* Fix webp by @lohanidamodar in https://github.com/appwrite/appwrite/pull/8732
* Ignore junction tables by @fogelito in https://github.com/appwrite/appwrite/pull/8728
* Fix logger throwing fatal error by @lohanidamodar in https://github.com/appwrite/appwrite/pull/8724
* Fix missing protocol for testing SMTP by @byawitz in https://github.com/appwrite/appwrite/pull/8749
* Make create execution async loose by @loks0n in https://github.com/appwrite/appwrite/pull/8707
* Fix invalid cursor value by @fogelito in https://github.com/appwrite/appwrite/pull/8109
* Fix target deletes by @abnegate in https://github.com/appwrite/appwrite/pull/8833
* Fix translation commas by @loks0n in https://github.com/appwrite/appwrite/pull/8892
* Fix Migrations having source creds being overwritten and add Migration tests by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8897
* Fix validator usage for updating string size by @abnegate in https://github.com/appwrite/appwrite/pull/8890
* Fix create user event not triggering by @loks0n in https://github.com/appwrite/appwrite/pull/8718
* Improve error handling and logging in the database worker by @fogelito in https://github.com/appwrite/appwrite/pull/8944
* Remove inaccurate info about leaving the URL parameter empty by @ebenezerdon in https://github.com/appwrite/appwrite/pull/8963
* Ensure indexes are updated when updating an attribute key by @fogelito in https://github.com/appwrite/appwrite/pull/8971
* Remove duplicate dart-2.16 runtime template by @stnguyen90 in https://github.com/appwrite/appwrite/pull/8972
* Fix team invites with existing session by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/9006
* Improve handling of HTTP requests by dispatching to safe workers by @Meldiron in https://github.com/appwrite/appwrite/pull/9016
* Fix users create session secret by @stnguyen90 in https://github.com/appwrite/appwrite/pull/9019
* Fix swoole task warning by @Meldiron in https://github.com/appwrite/appwrite/pull/9025
### Miscellaneous
* Update Init copy by @adityaoberai in https://github.com/appwrite/appwrite/pull/8557
* Fix security scan permissions and comment by @EVDOG4LIFE in https://github.com/appwrite/appwrite/pull/8525
* Add Trivy security scans by @btme0011 in https://github.com/appwrite/appwrite/pull/6876
* Update database stack by @abnegate in https://github.com/appwrite/appwrite/pull/8564
* Bump database by @abnegate in https://github.com/appwrite/appwrite/pull/8573
* Sync main with 1.5.x by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8589
* Add AWS to one-click installs by @byawitz in https://github.com/appwrite/appwrite/pull/8593
* Update Init copy in readme by @adityaoberai in https://github.com/appwrite/appwrite/pull/8618
* Sync main into 1.6.x by @stnguyen90 in https://github.com/appwrite/appwrite/pull/8685
* Sync 1.6.x into main by @stnguyen90 in https://github.com/appwrite/appwrite/pull/8686
* Feat coroutines by @Meldiron in https://github.com/appwrite/appwrite/pull/7826
* Sync main into 1.6.x by @Meldiron in https://github.com/appwrite/appwrite/pull/8719
* Sentence casing endpoint API reference by @choir241 in https://github.com/appwrite/appwrite/pull/8617
* DB storage metrics by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8404
* Fix exception thrown when optional array attribute does not exist by @lohanidamodar in https://github.com/appwrite/appwrite/pull/8391
* Add projects channels to realtime by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/8735
* Base for console roles support by @lohanidamodar in https://github.com/appwrite/appwrite/pull/8565
* Remove DB disk storage calculation by @christyjacob4 in https://github.com/appwrite/appwrite/pull/8745
* Messaging adapter default values by @shimonewman in https://github.com/appwrite/appwrite/pull/8742
* Add payload response type by @loks0n in https://github.com/appwrite/appwrite/pull/8720
* Fix flaky functions tests by @loks0n in https://github.com/appwrite/appwrite/pull/8682
* Migrations Backups by @fogelito in https://github.com/appwrite/appwrite/pull/8186
* Add test for response and request filters by @vermakhushboo in https://github.com/appwrite/appwrite/pull/8697
* Bump version in SECURITY.md by @EVDOG4LIFE in https://github.com/appwrite/appwrite/pull/8755
* Add originalId attribute to databases collection by @fogelito in https://github.com/appwrite/appwrite/pull/8764
* Fix Walter References by @ItzNotABug in https://github.com/appwrite/appwrite/pull/8757
* Update database by @abnegate in https://github.com/appwrite/appwrite/pull/8769
* Move new attributes by @abnegate in https://github.com/appwrite/appwrite/pull/8777
* Add ping endpoint by @loks0n in https://github.com/appwrite/appwrite/pull/8761
* Fix GitHub action caching by @loks0n in https://github.com/appwrite/appwrite/pull/8772
* Chore release ruby SDK by @abnegate in https://github.com/appwrite/appwrite/pull/8767
* Call migration success on success by @abnegate in https://github.com/appwrite/appwrite/pull/8782
* Update utopia-php/system to 0.9.0 by @basert in https://github.com/appwrite/appwrite/pull/8780
* Move createDocument from api to worker by @vermakhushboo in https://github.com/appwrite/appwrite/pull/8776
* Add missing indexes by @christyjacob4 in https://github.com/appwrite/appwrite/pull/8803
* Update database by @abnegate in https://github.com/appwrite/appwrite/pull/8809
* Fix typo in BLR region by @stnguyen90 in https://github.com/appwrite/appwrite/pull/8756
* Add tests for project variables by @vermakhushboo in https://github.com/appwrite/appwrite/pull/8815
* Replace 'Expires' with 'Cache-Control: private' header to avoid CDN caching by @basert in https://github.com/appwrite/appwrite/pull/8836
* Allow blocking based on resource attributes by @basert in https://github.com/appwrite/appwrite/pull/8812
* Check if resource is blocked inside functions worker by @basert in https://github.com/appwrite/appwrite/pull/8855
* Fix missing allow attribute by @abnegate in https://github.com/appwrite/appwrite/pull/8889
* Revert function execution order by @basert in https://github.com/appwrite/appwrite/pull/8857
* Use resource type constants by @basert in https://github.com/appwrite/appwrite/pull/8895
* Update Database lib by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8680
* Update database by @abnegate in https://github.com/appwrite/appwrite/pull/8917
* Update database by @abnegate in https://github.com/appwrite/appwrite/pull/8923
* Update database for transaction counter fixes with retries by @abnegate in https://github.com/appwrite/appwrite/pull/8927
* Validate string permissions by @fogelito in https://github.com/appwrite/appwrite/pull/8929
* Add PubSub adapter support by @basert in https://github.com/appwrite/appwrite/pull/8905
* List memberships as client by @loks0n in https://github.com/appwrite/appwrite/pull/8913
* Fix XDebug Extension not being removed by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8891
* Update database by @abnegate in https://github.com/appwrite/appwrite/pull/8946
* Use utopia compression by @loks0n in https://github.com/appwrite/appwrite/pull/8938
* Make compression minimum size configurable by @loks0n in https://github.com/appwrite/appwrite/pull/8947
* Revert "Update database" by @christyjacob4 in https://github.com/appwrite/appwrite/pull/8949
* Fix setpaused by @loks0n in https://github.com/appwrite/appwrite/pull/8948
* Use getDocument instead of find() for rules by @christyjacob4 in https://github.com/appwrite/appwrite/pull/8951
* Remove double fetch from migrations worker by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8956
* Fix memberships privacy MFA by @loks0n in https://github.com/appwrite/appwrite/pull/8969
* Add telemetry by @basert in https://github.com/appwrite/appwrite/pull/8960
* Send migration errors individually by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8959
* Add console sdk previews by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/8990
* Unset index length by @fogelito in https://github.com/appwrite/appwrite/pull/8978
* Update base to 0.9.5 by @basert in https://github.com/appwrite/appwrite/pull/9005
* Sync main into 1.6.x by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/9011
* Improved shared tables V2 by @abnegate in https://github.com/appwrite/appwrite/pull/9013
* Ensure backwards compatibility for 1.6.x by @christyjacob4 in https://github.com/appwrite/appwrite/pull/9018
# Version 1.6.0
## What's Changed
+12
View File
@@ -613,6 +613,18 @@ If you need to clear the cache, you can do so by running the following command:
docker compose exec redis redis-cli FLUSHALL
```
## Using preview domains locally
Appwrite Functions are automatically given a domain you can visit to execute the function. This domain has format `[SOMETHING].functions.localhost` unless you changed `_APP_DOMAIN_FUNCTIONS` environment variable. This default value works great when running Appwrite locally, but it can be impossible to use preview domains with Cloud woekspaces such as Gitpod or GitHub Codespaces.
To use preview domains on Cloud workspaces, you can visit hostname provided by them, and supply function's preview domain as URL parameter:
```
https://8080-appwrite-appwrite-mjeb3ebilwv.ws-eu116.gitpod.io/ping?preview=672b3c7eab1ac523ccf5.functions.localhost
```
The path was set to `/ping` intentionally. Visiting `/` for preview domains might trigger Console background worker, and trigger redirect to Console without our preview URL param. Visiting different path ensures this doesnt happen.
## Tutorials
From time to time, our team will add tutorials that will help contributors find their way in the Appwrite source code. Below is a list of currently available tutorials:
+1 -1
View File
@@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
--no-plugins --no-scripts --prefer-dist \
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
FROM appwrite/base:0.9.3 AS final
FROM appwrite/base:0.9.5 AS final
LABEL maintainer="team@appwrite.io"
+3 -3
View File
@@ -67,7 +67,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.6.0
appwrite/appwrite:1.6.1
```
### Windows
@@ -79,7 +79,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.6.0
appwrite/appwrite:1.6.1
```
#### PowerShell
@@ -89,7 +89,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.6.0
appwrite/appwrite:1.6.1
```
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
+3 -3
View File
@@ -75,7 +75,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.6.0
appwrite/appwrite:1.6.1
```
### Windows
@@ -87,7 +87,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.6.0
appwrite/appwrite:1.6.1
```
#### PowerShell
@@ -97,7 +97,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.6.0
appwrite/appwrite:1.6.1
```
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.
+14 -13
View File
@@ -52,7 +52,7 @@ CLI::setResource('pools', function (Registry $register) {
return $register->get('pools');
}, ['register']);
CLI::setResource('dbForConsole', function ($pools, $cache) {
CLI::setResource('dbForPlatform', function ($pools, $cache) {
$sleep = 3;
$maxAttempts = 5;
$attempts = 0;
@@ -67,9 +67,9 @@ CLI::setResource('dbForConsole', function ($pools, $cache) {
->pop()
->getResource();
$dbForConsole = new Database($dbAdapter, $cache);
$dbForPlatform = new Database($dbAdapter, $cache);
$dbForConsole
$dbForPlatform
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', 'console');
@@ -78,7 +78,7 @@ CLI::setResource('dbForConsole', function ($pools, $cache) {
$collections = Config::getParam('collections', [])['console'];
$last = \array_key_last($collections);
if (!($dbForConsole->exists($dbForConsole->getDatabase(), $last))) { /** TODO cache ready variable using registry */
if (!($dbForPlatform->exists($dbForPlatform->getDatabase(), $last))) { /** TODO cache ready variable using registry */
throw new Exception('Tables not ready yet.');
}
@@ -94,15 +94,15 @@ CLI::setResource('dbForConsole', function ($pools, $cache) {
throw new Exception("Console is not ready yet. Please try again later.");
}
return $dbForConsole;
return $dbForPlatform;
}, ['pools', 'cache']);
CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) {
CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) {
return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForConsole;
return $dbForPlatform;
}
try {
@@ -114,8 +114,9 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole,
if (isset($databases[$dsn->getHost()])) {
$database = $databases[$dsn->getHost()];
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -136,10 +137,10 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole,
->getResource();
$database = new Database($dbAdapter, $cache);
$databases[$dsn->getHost()] = $database;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -157,7 +158,7 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole,
return $database;
};
}, ['pools', 'dbForConsole', 'cache']);
}, ['pools', 'dbForPlatform', 'cache']);
CLI::setResource('queue', function (Group $pools) {
return $pools->get('queue')->pop()->getResource();
@@ -180,7 +181,7 @@ CLI::setResource('logError', function (Registry $register) {
$log = new Log();
$log->setNamespace($namespace);
$log->setServer(\gethostname());
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
+1 -1
View File
@@ -686,7 +686,7 @@ return [
],
Exception::ATTRIBUTE_LIMIT_EXCEEDED => [
'name' => Exception::ATTRIBUTE_LIMIT_EXCEEDED,
'description' => 'The maximum number of attributes has been reached.',
'description' => 'The maximum number or size of attributes for this collection has been reached.',
'code' => 400,
],
Exception::ATTRIBUTE_VALUE_INVALID => [
+1 -1
View File
@@ -11,7 +11,7 @@ const TEMPLATE_RUNTIMES = [
],
'DART' => [
'name' => 'dart',
'versions' => ['3.5', '3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16', '2.16']
'versions' => ['3.5', '3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16']
],
'GO' => [
'name' => 'go',
+5 -5
View File
@@ -11,7 +11,7 @@ return [
[
'key' => 'web',
'name' => 'Web',
'version' => '16.0.2',
'version' => '16.1.0',
'url' => 'https://github.com/appwrite/sdk-for-web',
'package' => 'https://www.npmjs.com/package/appwrite',
'enabled' => true,
@@ -59,7 +59,7 @@ return [
[
'key' => 'flutter',
'name' => 'Flutter',
'version' => '13.0.0',
'version' => '13.1.1',
'url' => 'https://github.com/appwrite/sdk-for-flutter',
'package' => 'https://pub.dev/packages/appwrite',
'enabled' => true,
@@ -77,7 +77,7 @@ return [
[
'key' => 'apple',
'name' => 'Apple',
'version' => '7.0.0',
'version' => '7.1.0',
'url' => 'https://github.com/appwrite/sdk-for-apple',
'package' => 'https://github.com/appwrite/sdk-for-apple',
'enabled' => true,
@@ -112,7 +112,7 @@ return [
[
'key' => 'android',
'name' => 'Android',
'version' => '6.0.0',
'version' => '6.1.0',
'url' => 'https://github.com/appwrite/sdk-for-android',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-android',
'enabled' => true,
@@ -134,7 +134,7 @@ return [
[
'key' => 'react-native',
'name' => 'React Native',
'version' => '0.5.0',
'version' => '0.6.0',
'url' => 'https://github.com/appwrite/sdk-for-react-native',
'package' => 'https://npmjs.com/package/react-native-appwrite',
'enabled' => true,
@@ -1,7 +1,7 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2784,7 +2784,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -5766,7 +5766,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 382,
"weight": 376,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5851,7 +5851,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 386,
"weight": 380,
"cookies": false,
"type": "",
"deprecated": false,
+72 -359
View File
@@ -1,7 +1,7 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2795,7 +2795,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -9488,7 +9488,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -10145,7 +10146,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -13673,7 +13675,7 @@
},
"x-appwrite": {
"method": "listMessages",
"weight": 390,
"weight": 384,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13751,7 +13753,7 @@
},
"x-appwrite": {
"method": "createEmail",
"weight": 387,
"weight": 381,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13897,7 +13899,7 @@
},
"x-appwrite": {
"method": "updateEmail",
"weight": 394,
"weight": 388,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14045,7 +14047,7 @@
},
"x-appwrite": {
"method": "createPush",
"weight": 389,
"weight": 383,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14202,7 +14204,7 @@
},
"x-appwrite": {
"method": "updatePush",
"weight": 396,
"weight": 390,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14361,7 +14363,7 @@
},
"x-appwrite": {
"method": "createSms",
"weight": 388,
"weight": 382,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14472,7 +14474,7 @@
},
"x-appwrite": {
"method": "updateSms",
"weight": 395,
"weight": 389,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14586,7 +14588,7 @@
},
"x-appwrite": {
"method": "getMessage",
"weight": 393,
"weight": 387,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14641,7 +14643,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 397,
"weight": 391,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14705,7 +14707,7 @@
},
"x-appwrite": {
"method": "listMessageLogs",
"weight": 391,
"weight": 385,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14782,7 +14784,7 @@
},
"x-appwrite": {
"method": "listTargets",
"weight": 392,
"weight": 386,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14859,7 +14861,7 @@
},
"x-appwrite": {
"method": "listProviders",
"weight": 362,
"weight": 356,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14937,7 +14939,7 @@
},
"x-appwrite": {
"method": "createApnsProvider",
"weight": 361,
"weight": 355,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15044,7 +15046,7 @@
},
"x-appwrite": {
"method": "updateApnsProvider",
"weight": 374,
"weight": 368,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15154,7 +15156,7 @@
},
"x-appwrite": {
"method": "createFcmProvider",
"weight": 360,
"weight": 354,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15241,7 +15243,7 @@
},
"x-appwrite": {
"method": "updateFcmProvider",
"weight": 373,
"weight": 367,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15331,7 +15333,7 @@
},
"x-appwrite": {
"method": "createMailgunProvider",
"weight": 352,
"weight": 346,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15448,7 +15450,7 @@
},
"x-appwrite": {
"method": "updateMailgunProvider",
"weight": 365,
"weight": 359,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15568,7 +15570,7 @@
},
"x-appwrite": {
"method": "createMsg91Provider",
"weight": 355,
"weight": 349,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15665,7 +15667,7 @@
},
"x-appwrite": {
"method": "updateMsg91Provider",
"weight": 368,
"weight": 362,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15765,7 +15767,7 @@
},
"x-appwrite": {
"method": "createSendgridProvider",
"weight": 353,
"weight": 347,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15872,7 +15874,7 @@
},
"x-appwrite": {
"method": "updateSendgridProvider",
"weight": 366,
"weight": 360,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15982,7 +15984,7 @@
},
"x-appwrite": {
"method": "createSmtpProvider",
"weight": 354,
"weight": 348,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16127,7 +16129,7 @@
},
"x-appwrite": {
"method": "updateSmtpProvider",
"weight": 367,
"weight": 361,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16274,7 +16276,7 @@
},
"x-appwrite": {
"method": "createTelesignProvider",
"weight": 356,
"weight": 350,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16371,7 +16373,7 @@
},
"x-appwrite": {
"method": "updateTelesignProvider",
"weight": 369,
"weight": 363,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16471,7 +16473,7 @@
},
"x-appwrite": {
"method": "createTextmagicProvider",
"weight": 357,
"weight": 351,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16568,7 +16570,7 @@
},
"x-appwrite": {
"method": "updateTextmagicProvider",
"weight": 370,
"weight": 364,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16668,7 +16670,7 @@
},
"x-appwrite": {
"method": "createTwilioProvider",
"weight": 358,
"weight": 352,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16765,7 +16767,7 @@
},
"x-appwrite": {
"method": "updateTwilioProvider",
"weight": 371,
"weight": 365,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16865,7 +16867,7 @@
},
"x-appwrite": {
"method": "createVonageProvider",
"weight": 359,
"weight": 353,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16962,7 +16964,7 @@
},
"x-appwrite": {
"method": "updateVonageProvider",
"weight": 372,
"weight": 366,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17062,7 +17064,7 @@
},
"x-appwrite": {
"method": "getProvider",
"weight": 364,
"weight": 358,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17117,7 +17119,7 @@
},
"x-appwrite": {
"method": "deleteProvider",
"weight": 375,
"weight": 369,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17181,7 +17183,7 @@
},
"x-appwrite": {
"method": "listProviderLogs",
"weight": 363,
"weight": 357,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17258,7 +17260,7 @@
},
"x-appwrite": {
"method": "listSubscriberLogs",
"weight": 384,
"weight": 378,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17335,7 +17337,7 @@
},
"x-appwrite": {
"method": "listTopics",
"weight": 377,
"weight": 371,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17411,7 +17413,7 @@
},
"x-appwrite": {
"method": "createTopic",
"weight": 376,
"weight": 370,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17496,7 +17498,7 @@
},
"x-appwrite": {
"method": "getTopic",
"weight": 379,
"weight": 373,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17558,7 +17560,7 @@
},
"x-appwrite": {
"method": "updateTopic",
"weight": 380,
"weight": 374,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17637,7 +17639,7 @@
},
"x-appwrite": {
"method": "deleteTopic",
"weight": 381,
"weight": 375,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17701,7 +17703,7 @@
},
"x-appwrite": {
"method": "listTopicLogs",
"weight": 378,
"weight": 372,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17778,7 +17780,7 @@
},
"x-appwrite": {
"method": "listSubscribers",
"weight": 383,
"weight": 377,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17864,7 +17866,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 382,
"weight": 376,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17956,7 +17958,7 @@
},
"x-appwrite": {
"method": "getSubscriber",
"weight": 385,
"weight": 379,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18021,7 +18023,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 386,
"weight": 380,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18098,7 +18100,7 @@
},
"x-appwrite": {
"method": "list",
"weight": 339,
"weight": 338,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18264,7 +18266,7 @@
},
"x-appwrite": {
"method": "getAppwriteReport",
"weight": 341,
"weight": 340,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18339,7 +18341,7 @@
},
"\/migrations\/firebase": {
"post": {
"summary": "Migrate Firebase data (Service Account)",
"summary": "Migrate Firebase data",
"operationId": "migrationsCreateFirebaseMigration",
"tags": [
"migrations"
@@ -18359,7 +18361,7 @@
},
"x-appwrite": {
"method": "createFirebaseMigration",
"weight": 336,
"weight": 335,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18415,177 +18417,6 @@
}
}
},
"\/migrations\/firebase\/deauthorize": {
"get": {
"summary": "Revoke Appwrite's authorization to access Firebase projects",
"operationId": "migrationsDeleteFirebaseAuth",
"tags": [
"migrations"
],
"description": "",
"responses": {
"200": {
"description": "File"
}
},
"x-appwrite": {
"method": "deleteFirebaseAuth",
"weight": 347,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "migrations\/delete-firebase-auth.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "migrations.write",
"platforms": [
"console"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
]
}
},
"\/migrations\/firebase\/oauth": {
"post": {
"summary": "Migrate Firebase data (OAuth)",
"operationId": "migrationsCreateFirebaseOAuthMigration",
"tags": [
"migrations"
],
"description": "",
"responses": {
"202": {
"description": "Migration",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/migration"
}
}
}
}
},
"x-appwrite": {
"method": "createFirebaseOAuthMigration",
"weight": 335,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "migrations\/create-firebase-o-auth-migration.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/migrations\/migration-firebase.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "migrations.write",
"platforms": [
"console"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
],
"requestBody": {
"content": {
"application\/json": {
"schema": {
"type": "object",
"properties": {
"resources": {
"type": "array",
"description": "List of resources to migrate",
"x-example": null,
"items": {
"type": "string"
}
},
"projectId": {
"type": "string",
"description": "Project ID of the Firebase Project",
"x-example": "<PROJECT_ID>"
}
},
"required": [
"resources",
"projectId"
]
}
}
}
}
}
},
"\/migrations\/firebase\/projects": {
"get": {
"summary": "List Firebase projects",
"operationId": "migrationsListFirebaseProjects",
"tags": [
"migrations"
],
"description": "",
"responses": {
"200": {
"description": "Migrations Firebase Projects List",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/firebaseProjectList"
}
}
}
}
},
"x-appwrite": {
"method": "listFirebaseProjects",
"weight": 346,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "migrations\/list-firebase-projects.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "migrations.read",
"platforms": [
"console"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
]
}
},
"\/migrations\/firebase\/report": {
"get": {
"summary": "Generate a report on Firebase data",
@@ -18608,7 +18439,7 @@
},
"x-appwrite": {
"method": "getFirebaseReport",
"weight": 342,
"weight": 341,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18660,80 +18491,6 @@
]
}
},
"\/migrations\/firebase\/report\/oauth": {
"get": {
"summary": "Generate a report on Firebase data using OAuth",
"operationId": "migrationsGetFirebaseReportOAuth",
"tags": [
"migrations"
],
"description": "",
"responses": {
"200": {
"description": "Migration Report",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/migrationReport"
}
}
}
}
},
"x-appwrite": {
"method": "getFirebaseReportOAuth",
"weight": 343,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "migrations\/get-firebase-report-o-auth.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/migrations\/migration-firebase-report.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "migrations.write",
"platforms": [
"console"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
],
"parameters": [
{
"name": "resources",
"description": "List of resources to migrate",
"required": true,
"schema": {
"type": "array",
"items": {
"type": "string"
}
},
"in": "query"
},
{
"name": "projectId",
"description": "Project ID",
"required": true,
"schema": {
"type": "string",
"x-example": "<PROJECT_ID>"
},
"in": "query"
}
]
}
},
"\/migrations\/nhost": {
"post": {
"summary": "Migrate NHost data",
@@ -18756,7 +18513,7 @@
},
"x-appwrite": {
"method": "createNHostMigration",
"weight": 338,
"weight": 337,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18869,7 +18626,7 @@
},
"x-appwrite": {
"method": "getNHostReport",
"weight": 349,
"weight": 343,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19004,7 +18761,7 @@
},
"x-appwrite": {
"method": "createSupabaseMigration",
"weight": 337,
"weight": 336,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19111,7 +18868,7 @@
},
"x-appwrite": {
"method": "getSupabaseReport",
"weight": 348,
"weight": 342,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19237,7 +18994,7 @@
},
"x-appwrite": {
"method": "get",
"weight": 340,
"weight": 339,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19297,7 +19054,7 @@
},
"x-appwrite": {
"method": "retry",
"weight": 350,
"weight": 344,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19350,7 +19107,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 351,
"weight": 345,
"cookies": false,
"type": "",
"deprecated": false,
@@ -20672,7 +20429,7 @@
},
"\/projects\/{projectId}\/auth\/memberships-privacy": {
"patch": {
"summary": "Update project team sensitive attributes",
"summary": "Update project memberships privacy attributes",
"operationId": "projectsUpdateMembershipsPrivacy",
"tags": [
"projects"
@@ -32749,30 +32506,6 @@
"migrations"
]
},
"firebaseProjectList": {
"description": "Migrations Firebase Projects List",
"type": "object",
"properties": {
"total": {
"type": "integer",
"description": "Total number of projects documents that matched your query.",
"x-example": 5,
"format": "int32"
},
"projects": {
"type": "array",
"description": "List of projects.",
"items": {
"$ref": "#\/components\/schemas\/firebaseProject"
},
"x-example": ""
}
},
"required": [
"total",
"projects"
]
},
"specificationList": {
"description": "Specifications List",
"type": "object",
@@ -35109,7 +34842,7 @@
},
"schedule": {
"type": "string",
"description": "Function execution schedult in CRON format.",
"description": "Function execution schedule in CRON format.",
"x-example": "5 4 * * *"
},
"timeout": {
@@ -36077,17 +35810,17 @@
"description": "Whether or not to send session alert emails to users.",
"x-example": true
},
"membershipsUserName": {
"authMembershipsUserName": {
"type": "boolean",
"description": "Whether or not to show user names in the teams membership response.",
"x-example": true
},
"membershipsUserEmail": {
"authMembershipsUserEmail": {
"type": "boolean",
"description": "Whether or not to show user emails in the teams membership response.",
"x-example": true
},
"membershipsMfa": {
"authMembershipsMfa": {
"type": "boolean",
"description": "Whether or not to show user MFA status in the teams membership response.",
"x-example": true
@@ -36297,9 +36030,9 @@
"authPersonalDataCheck",
"authMockNumbers",
"authSessionAlerts",
"membershipsUserName",
"membershipsUserEmail",
"membershipsMfa",
"authMembershipsUserName",
"authMembershipsUserEmail",
"authMembershipsMfa",
"oAuthProviders",
"platforms",
"webhooks",
@@ -38707,26 +38440,6 @@
"size",
"version"
]
},
"firebaseProject": {
"description": "MigrationFirebaseProject",
"type": "object",
"properties": {
"projectId": {
"type": "string",
"description": "Project ID.",
"x-example": "my-project"
},
"displayName": {
"type": "string",
"description": "Project display name.",
"x-example": "My Project"
}
},
"required": [
"projectId",
"displayName"
]
}
},
"securitySchemes": {
+53 -51
View File
@@ -1,7 +1,7 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2451,7 +2451,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -8596,7 +8596,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -9019,7 +9020,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -12527,7 +12529,7 @@
},
"x-appwrite": {
"method": "listMessages",
"weight": 390,
"weight": 384,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12606,7 +12608,7 @@
},
"x-appwrite": {
"method": "createEmail",
"weight": 387,
"weight": 381,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12753,7 +12755,7 @@
},
"x-appwrite": {
"method": "updateEmail",
"weight": 394,
"weight": 388,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12902,7 +12904,7 @@
},
"x-appwrite": {
"method": "createPush",
"weight": 389,
"weight": 383,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13060,7 +13062,7 @@
},
"x-appwrite": {
"method": "updatePush",
"weight": 396,
"weight": 390,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13220,7 +13222,7 @@
},
"x-appwrite": {
"method": "createSms",
"weight": 388,
"weight": 382,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13332,7 +13334,7 @@
},
"x-appwrite": {
"method": "updateSms",
"weight": 395,
"weight": 389,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13447,7 +13449,7 @@
},
"x-appwrite": {
"method": "getMessage",
"weight": 393,
"weight": 387,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13503,7 +13505,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 397,
"weight": 391,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13568,7 +13570,7 @@
},
"x-appwrite": {
"method": "listMessageLogs",
"weight": 391,
"weight": 385,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13646,7 +13648,7 @@
},
"x-appwrite": {
"method": "listTargets",
"weight": 392,
"weight": 386,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13724,7 +13726,7 @@
},
"x-appwrite": {
"method": "listProviders",
"weight": 362,
"weight": 356,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13803,7 +13805,7 @@
},
"x-appwrite": {
"method": "createApnsProvider",
"weight": 361,
"weight": 355,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13911,7 +13913,7 @@
},
"x-appwrite": {
"method": "updateApnsProvider",
"weight": 374,
"weight": 368,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14022,7 +14024,7 @@
},
"x-appwrite": {
"method": "createFcmProvider",
"weight": 360,
"weight": 354,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14110,7 +14112,7 @@
},
"x-appwrite": {
"method": "updateFcmProvider",
"weight": 373,
"weight": 367,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14201,7 +14203,7 @@
},
"x-appwrite": {
"method": "createMailgunProvider",
"weight": 352,
"weight": 346,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14319,7 +14321,7 @@
},
"x-appwrite": {
"method": "updateMailgunProvider",
"weight": 365,
"weight": 359,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14440,7 +14442,7 @@
},
"x-appwrite": {
"method": "createMsg91Provider",
"weight": 355,
"weight": 349,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14538,7 +14540,7 @@
},
"x-appwrite": {
"method": "updateMsg91Provider",
"weight": 368,
"weight": 362,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14639,7 +14641,7 @@
},
"x-appwrite": {
"method": "createSendgridProvider",
"weight": 353,
"weight": 347,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14747,7 +14749,7 @@
},
"x-appwrite": {
"method": "updateSendgridProvider",
"weight": 366,
"weight": 360,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14858,7 +14860,7 @@
},
"x-appwrite": {
"method": "createSmtpProvider",
"weight": 354,
"weight": 348,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15004,7 +15006,7 @@
},
"x-appwrite": {
"method": "updateSmtpProvider",
"weight": 367,
"weight": 361,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15152,7 +15154,7 @@
},
"x-appwrite": {
"method": "createTelesignProvider",
"weight": 356,
"weight": 350,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15250,7 +15252,7 @@
},
"x-appwrite": {
"method": "updateTelesignProvider",
"weight": 369,
"weight": 363,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15351,7 +15353,7 @@
},
"x-appwrite": {
"method": "createTextmagicProvider",
"weight": 357,
"weight": 351,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15449,7 +15451,7 @@
},
"x-appwrite": {
"method": "updateTextmagicProvider",
"weight": 370,
"weight": 364,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15550,7 +15552,7 @@
},
"x-appwrite": {
"method": "createTwilioProvider",
"weight": 358,
"weight": 352,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15648,7 +15650,7 @@
},
"x-appwrite": {
"method": "updateTwilioProvider",
"weight": 371,
"weight": 365,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15749,7 +15751,7 @@
},
"x-appwrite": {
"method": "createVonageProvider",
"weight": 359,
"weight": 353,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15847,7 +15849,7 @@
},
"x-appwrite": {
"method": "updateVonageProvider",
"weight": 372,
"weight": 366,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15948,7 +15950,7 @@
},
"x-appwrite": {
"method": "getProvider",
"weight": 364,
"weight": 358,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16004,7 +16006,7 @@
},
"x-appwrite": {
"method": "deleteProvider",
"weight": 375,
"weight": 369,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16069,7 +16071,7 @@
},
"x-appwrite": {
"method": "listProviderLogs",
"weight": 363,
"weight": 357,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16147,7 +16149,7 @@
},
"x-appwrite": {
"method": "listSubscriberLogs",
"weight": 384,
"weight": 378,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16225,7 +16227,7 @@
},
"x-appwrite": {
"method": "listTopics",
"weight": 377,
"weight": 371,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16302,7 +16304,7 @@
},
"x-appwrite": {
"method": "createTopic",
"weight": 376,
"weight": 370,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16388,7 +16390,7 @@
},
"x-appwrite": {
"method": "getTopic",
"weight": 379,
"weight": 373,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16451,7 +16453,7 @@
},
"x-appwrite": {
"method": "updateTopic",
"weight": 380,
"weight": 374,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16531,7 +16533,7 @@
},
"x-appwrite": {
"method": "deleteTopic",
"weight": 381,
"weight": 375,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16596,7 +16598,7 @@
},
"x-appwrite": {
"method": "listTopicLogs",
"weight": 378,
"weight": 372,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16674,7 +16676,7 @@
},
"x-appwrite": {
"method": "listSubscribers",
"weight": 383,
"weight": 377,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16761,7 +16763,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 382,
"weight": 376,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16855,7 +16857,7 @@
},
"x-appwrite": {
"method": "getSubscriber",
"weight": 385,
"weight": 379,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16921,7 +16923,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 386,
"weight": 380,
"cookies": false,
"type": "",
"deprecated": false,
@@ -25746,7 +25748,7 @@
},
"schedule": {
"type": "string",
"description": "Function execution schedult in CRON format.",
"description": "Function execution schedule in CRON format.",
"x-example": "5 4 * * *"
},
"timeout": {
+4 -4
View File
@@ -1,7 +1,7 @@
{
"swagger": "2.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2922,7 +2922,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -5981,7 +5981,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 382,
"weight": 376,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6070,7 +6070,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 386,
"weight": 380,
"cookies": false,
"type": "",
"deprecated": false,
+72 -374
View File
@@ -1,7 +1,7 @@
{
"swagger": "2.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2949,7 +2949,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -9610,7 +9610,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -10286,7 +10287,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -13890,7 +13892,7 @@
},
"x-appwrite": {
"method": "listMessages",
"weight": 390,
"weight": 384,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13967,7 +13969,7 @@
},
"x-appwrite": {
"method": "createEmail",
"weight": 387,
"weight": 381,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14127,7 +14129,7 @@
},
"x-appwrite": {
"method": "updateEmail",
"weight": 394,
"weight": 388,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14284,7 +14286,7 @@
},
"x-appwrite": {
"method": "createPush",
"weight": 389,
"weight": 383,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14459,7 +14461,7 @@
},
"x-appwrite": {
"method": "updatePush",
"weight": 396,
"weight": 390,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14631,7 +14633,7 @@
},
"x-appwrite": {
"method": "createSms",
"weight": 388,
"weight": 382,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14751,7 +14753,7 @@
},
"x-appwrite": {
"method": "updateSms",
"weight": 395,
"weight": 389,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14869,7 +14871,7 @@
},
"x-appwrite": {
"method": "getMessage",
"weight": 393,
"weight": 387,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14928,7 +14930,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 397,
"weight": 391,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14992,7 +14994,7 @@
},
"x-appwrite": {
"method": "listMessageLogs",
"weight": 391,
"weight": 385,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15068,7 +15070,7 @@
},
"x-appwrite": {
"method": "listTargets",
"weight": 392,
"weight": 386,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15144,7 +15146,7 @@
},
"x-appwrite": {
"method": "listProviders",
"weight": 362,
"weight": 356,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15221,7 +15223,7 @@
},
"x-appwrite": {
"method": "createApnsProvider",
"weight": 361,
"weight": 355,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15338,7 +15340,7 @@
},
"x-appwrite": {
"method": "updateApnsProvider",
"weight": 374,
"weight": 368,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15453,7 +15455,7 @@
},
"x-appwrite": {
"method": "createFcmProvider",
"weight": 360,
"weight": 354,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15546,7 +15548,7 @@
},
"x-appwrite": {
"method": "updateFcmProvider",
"weight": 373,
"weight": 367,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15637,7 +15639,7 @@
},
"x-appwrite": {
"method": "createMailgunProvider",
"weight": 352,
"weight": 346,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15766,7 +15768,7 @@
},
"x-appwrite": {
"method": "updateMailgunProvider",
"weight": 365,
"weight": 359,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15893,7 +15895,7 @@
},
"x-appwrite": {
"method": "createMsg91Provider",
"weight": 355,
"weight": 349,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15998,7 +16000,7 @@
},
"x-appwrite": {
"method": "updateMsg91Provider",
"weight": 368,
"weight": 362,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16101,7 +16103,7 @@
},
"x-appwrite": {
"method": "createSendgridProvider",
"weight": 353,
"weight": 347,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16218,7 +16220,7 @@
},
"x-appwrite": {
"method": "updateSendgridProvider",
"weight": 366,
"weight": 360,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16333,7 +16335,7 @@
},
"x-appwrite": {
"method": "createSmtpProvider",
"weight": 354,
"weight": 348,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16494,7 +16496,7 @@
},
"x-appwrite": {
"method": "updateSmtpProvider",
"weight": 367,
"weight": 361,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16652,7 +16654,7 @@
},
"x-appwrite": {
"method": "createTelesignProvider",
"weight": 356,
"weight": 350,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16757,7 +16759,7 @@
},
"x-appwrite": {
"method": "updateTelesignProvider",
"weight": 369,
"weight": 363,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16860,7 +16862,7 @@
},
"x-appwrite": {
"method": "createTextmagicProvider",
"weight": 357,
"weight": 351,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16965,7 +16967,7 @@
},
"x-appwrite": {
"method": "updateTextmagicProvider",
"weight": 370,
"weight": 364,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17068,7 +17070,7 @@
},
"x-appwrite": {
"method": "createTwilioProvider",
"weight": 358,
"weight": 352,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17173,7 +17175,7 @@
},
"x-appwrite": {
"method": "updateTwilioProvider",
"weight": 371,
"weight": 365,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17276,7 +17278,7 @@
},
"x-appwrite": {
"method": "createVonageProvider",
"weight": 359,
"weight": 353,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17381,7 +17383,7 @@
},
"x-appwrite": {
"method": "updateVonageProvider",
"weight": 372,
"weight": 366,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17484,7 +17486,7 @@
},
"x-appwrite": {
"method": "getProvider",
"weight": 364,
"weight": 358,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17543,7 +17545,7 @@
},
"x-appwrite": {
"method": "deleteProvider",
"weight": 375,
"weight": 369,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17607,7 +17609,7 @@
},
"x-appwrite": {
"method": "listProviderLogs",
"weight": 363,
"weight": 357,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17683,7 +17685,7 @@
},
"x-appwrite": {
"method": "listSubscriberLogs",
"weight": 384,
"weight": 378,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17759,7 +17761,7 @@
},
"x-appwrite": {
"method": "listTopics",
"weight": 377,
"weight": 371,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17834,7 +17836,7 @@
},
"x-appwrite": {
"method": "createTopic",
"weight": 376,
"weight": 370,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17926,7 +17928,7 @@
},
"x-appwrite": {
"method": "getTopic",
"weight": 379,
"weight": 373,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17988,7 +17990,7 @@
},
"x-appwrite": {
"method": "updateTopic",
"weight": 380,
"weight": 374,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18071,7 +18073,7 @@
},
"x-appwrite": {
"method": "deleteTopic",
"weight": 381,
"weight": 375,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18135,7 +18137,7 @@
},
"x-appwrite": {
"method": "listTopicLogs",
"weight": 378,
"weight": 372,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18211,7 +18213,7 @@
},
"x-appwrite": {
"method": "listSubscribers",
"weight": 383,
"weight": 377,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18294,7 +18296,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 382,
"weight": 376,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18386,7 +18388,7 @@
},
"x-appwrite": {
"method": "getSubscriber",
"weight": 385,
"weight": 379,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18453,7 +18455,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 386,
"weight": 380,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18528,7 +18530,7 @@
},
"x-appwrite": {
"method": "list",
"weight": 339,
"weight": 338,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18699,7 +18701,7 @@
},
"x-appwrite": {
"method": "getAppwriteReport",
"weight": 341,
"weight": 340,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18767,7 +18769,7 @@
},
"\/migrations\/firebase": {
"post": {
"summary": "Migrate Firebase data (Service Account)",
"summary": "Migrate Firebase data",
"operationId": "migrationsCreateFirebaseMigration",
"consumes": [
"application\/json"
@@ -18789,7 +18791,7 @@
},
"x-appwrite": {
"method": "createFirebaseMigration",
"weight": 336,
"weight": 335,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18847,192 +18849,6 @@
]
}
},
"\/migrations\/firebase\/deauthorize": {
"get": {
"summary": "Revoke Appwrite's authorization to access Firebase projects",
"operationId": "migrationsDeleteFirebaseAuth",
"consumes": [
"application\/json"
],
"produces": [
"application\/json"
],
"tags": [
"migrations"
],
"description": "",
"responses": {
"200": {
"description": "File",
"schema": {
"type": "file"
}
}
},
"x-appwrite": {
"method": "deleteFirebaseAuth",
"weight": 347,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "migrations\/delete-firebase-auth.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "migrations.write",
"platforms": [
"console"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
]
}
},
"\/migrations\/firebase\/oauth": {
"post": {
"summary": "Migrate Firebase data (OAuth)",
"operationId": "migrationsCreateFirebaseOAuthMigration",
"consumes": [
"application\/json"
],
"produces": [
"application\/json"
],
"tags": [
"migrations"
],
"description": "",
"responses": {
"202": {
"description": "Migration",
"schema": {
"$ref": "#\/definitions\/migration"
}
}
},
"x-appwrite": {
"method": "createFirebaseOAuthMigration",
"weight": 335,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "migrations\/create-firebase-o-auth-migration.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/migrations\/migration-firebase.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "migrations.write",
"platforms": [
"console"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
],
"parameters": [
{
"name": "payload",
"in": "body",
"schema": {
"type": "object",
"properties": {
"resources": {
"type": "array",
"description": "List of resources to migrate",
"default": null,
"x-example": null,
"items": {
"type": "string"
}
},
"projectId": {
"type": "string",
"description": "Project ID of the Firebase Project",
"default": null,
"x-example": "<PROJECT_ID>"
}
},
"required": [
"resources",
"projectId"
]
}
}
]
}
},
"\/migrations\/firebase\/projects": {
"get": {
"summary": "List Firebase projects",
"operationId": "migrationsListFirebaseProjects",
"consumes": [
"application\/json"
],
"produces": [
"application\/json"
],
"tags": [
"migrations"
],
"description": "",
"responses": {
"200": {
"description": "Migrations Firebase Projects List",
"schema": {
"$ref": "#\/definitions\/firebaseProjectList"
}
}
},
"x-appwrite": {
"method": "listFirebaseProjects",
"weight": 346,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "migrations\/list-firebase-projects.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "migrations.read",
"platforms": [
"console"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
]
}
},
"\/migrations\/firebase\/report": {
"get": {
"summary": "Generate a report on Firebase data",
@@ -19057,7 +18873,7 @@
},
"x-appwrite": {
"method": "getFirebaseReport",
"weight": 342,
"weight": 341,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19106,79 +18922,6 @@
]
}
},
"\/migrations\/firebase\/report\/oauth": {
"get": {
"summary": "Generate a report on Firebase data using OAuth",
"operationId": "migrationsGetFirebaseReportOAuth",
"consumes": [
"application\/json"
],
"produces": [
"application\/json"
],
"tags": [
"migrations"
],
"description": "",
"responses": {
"200": {
"description": "Migration Report",
"schema": {
"$ref": "#\/definitions\/migrationReport"
}
}
},
"x-appwrite": {
"method": "getFirebaseReportOAuth",
"weight": 343,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "migrations\/get-firebase-report-o-auth.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/migrations\/migration-firebase-report.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "migrations.write",
"platforms": [
"console"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
],
"parameters": [
{
"name": "resources",
"description": "List of resources to migrate",
"required": true,
"type": "array",
"collectionFormat": "multi",
"items": {
"type": "string"
},
"in": "query"
},
{
"name": "projectId",
"description": "Project ID",
"required": true,
"type": "string",
"x-example": "<PROJECT_ID>",
"in": "query"
}
]
}
},
"\/migrations\/nhost": {
"post": {
"summary": "Migrate NHost data",
@@ -19203,7 +18946,7 @@
},
"x-appwrite": {
"method": "createNHostMigration",
"weight": 338,
"weight": 337,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19326,7 +19069,7 @@
},
"x-appwrite": {
"method": "getNHostReport",
"weight": 349,
"weight": 343,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19448,7 +19191,7 @@
},
"x-appwrite": {
"method": "createSupabaseMigration",
"weight": 337,
"weight": 336,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19564,7 +19307,7 @@
},
"x-appwrite": {
"method": "getSupabaseReport",
"weight": 348,
"weight": 342,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19679,7 +19422,7 @@
},
"x-appwrite": {
"method": "get",
"weight": 340,
"weight": 339,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19739,7 +19482,7 @@
},
"x-appwrite": {
"method": "retry",
"weight": 350,
"weight": 344,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19794,7 +19537,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 351,
"weight": 345,
"cookies": false,
"type": "",
"deprecated": false,
@@ -21138,7 +20881,7 @@
},
"\/projects\/{projectId}\/auth\/memberships-privacy": {
"patch": {
"summary": "Update project team sensitive attributes",
"summary": "Update project memberships privacy attributes",
"operationId": "projectsUpdateMembershipsPrivacy",
"consumes": [
"application\/json"
@@ -33250,31 +32993,6 @@
"migrations"
]
},
"firebaseProjectList": {
"description": "Migrations Firebase Projects List",
"type": "object",
"properties": {
"total": {
"type": "integer",
"description": "Total number of projects documents that matched your query.",
"x-example": 5,
"format": "int32"
},
"projects": {
"type": "array",
"description": "List of projects.",
"items": {
"type": "object",
"$ref": "#\/definitions\/firebaseProject"
},
"x-example": ""
}
},
"required": [
"total",
"projects"
]
},
"specificationList": {
"description": "Specifications List",
"type": "object",
@@ -35618,7 +35336,7 @@
},
"schedule": {
"type": "string",
"description": "Function execution schedult in CRON format.",
"description": "Function execution schedule in CRON format.",
"x-example": "5 4 * * *"
},
"timeout": {
@@ -36591,17 +36309,17 @@
"description": "Whether or not to send session alert emails to users.",
"x-example": true
},
"membershipsUserName": {
"authMembershipsUserName": {
"type": "boolean",
"description": "Whether or not to show user names in the teams membership response.",
"x-example": true
},
"membershipsUserEmail": {
"authMembershipsUserEmail": {
"type": "boolean",
"description": "Whether or not to show user emails in the teams membership response.",
"x-example": true
},
"membershipsMfa": {
"authMembershipsMfa": {
"type": "boolean",
"description": "Whether or not to show user MFA status in the teams membership response.",
"x-example": true
@@ -36815,9 +36533,9 @@
"authPersonalDataCheck",
"authMockNumbers",
"authSessionAlerts",
"membershipsUserName",
"membershipsUserEmail",
"membershipsMfa",
"authMembershipsUserName",
"authMembershipsUserEmail",
"authMembershipsMfa",
"oAuthProviders",
"platforms",
"webhooks",
@@ -39274,26 +38992,6 @@
"size",
"version"
]
},
"firebaseProject": {
"description": "MigrationFirebaseProject",
"type": "object",
"properties": {
"projectId": {
"type": "string",
"description": "Project ID.",
"x-example": "my-project"
},
"displayName": {
"type": "string",
"description": "Project display name.",
"x-example": "My Project"
}
},
"required": [
"projectId",
"displayName"
]
}
},
"externalDocs": {
+53 -51
View File
@@ -1,7 +1,7 @@
{
"swagger": "2.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2604,7 +2604,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -8720,7 +8720,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -9166,7 +9167,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -12752,7 +12754,7 @@
},
"x-appwrite": {
"method": "listMessages",
"weight": 390,
"weight": 384,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12830,7 +12832,7 @@
},
"x-appwrite": {
"method": "createEmail",
"weight": 387,
"weight": 381,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12991,7 +12993,7 @@
},
"x-appwrite": {
"method": "updateEmail",
"weight": 394,
"weight": 388,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13149,7 +13151,7 @@
},
"x-appwrite": {
"method": "createPush",
"weight": 389,
"weight": 383,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13325,7 +13327,7 @@
},
"x-appwrite": {
"method": "updatePush",
"weight": 396,
"weight": 390,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13498,7 +13500,7 @@
},
"x-appwrite": {
"method": "createSms",
"weight": 388,
"weight": 382,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13619,7 +13621,7 @@
},
"x-appwrite": {
"method": "updateSms",
"weight": 395,
"weight": 389,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13738,7 +13740,7 @@
},
"x-appwrite": {
"method": "getMessage",
"weight": 393,
"weight": 387,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13798,7 +13800,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 397,
"weight": 391,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13863,7 +13865,7 @@
},
"x-appwrite": {
"method": "listMessageLogs",
"weight": 391,
"weight": 385,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13940,7 +13942,7 @@
},
"x-appwrite": {
"method": "listTargets",
"weight": 392,
"weight": 386,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14017,7 +14019,7 @@
},
"x-appwrite": {
"method": "listProviders",
"weight": 362,
"weight": 356,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14095,7 +14097,7 @@
},
"x-appwrite": {
"method": "createApnsProvider",
"weight": 361,
"weight": 355,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14213,7 +14215,7 @@
},
"x-appwrite": {
"method": "updateApnsProvider",
"weight": 374,
"weight": 368,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14329,7 +14331,7 @@
},
"x-appwrite": {
"method": "createFcmProvider",
"weight": 360,
"weight": 354,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14423,7 +14425,7 @@
},
"x-appwrite": {
"method": "updateFcmProvider",
"weight": 373,
"weight": 367,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14515,7 +14517,7 @@
},
"x-appwrite": {
"method": "createMailgunProvider",
"weight": 352,
"weight": 346,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14645,7 +14647,7 @@
},
"x-appwrite": {
"method": "updateMailgunProvider",
"weight": 365,
"weight": 359,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14773,7 +14775,7 @@
},
"x-appwrite": {
"method": "createMsg91Provider",
"weight": 355,
"weight": 349,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14879,7 +14881,7 @@
},
"x-appwrite": {
"method": "updateMsg91Provider",
"weight": 368,
"weight": 362,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14983,7 +14985,7 @@
},
"x-appwrite": {
"method": "createSendgridProvider",
"weight": 353,
"weight": 347,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15101,7 +15103,7 @@
},
"x-appwrite": {
"method": "updateSendgridProvider",
"weight": 366,
"weight": 360,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15217,7 +15219,7 @@
},
"x-appwrite": {
"method": "createSmtpProvider",
"weight": 354,
"weight": 348,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15379,7 +15381,7 @@
},
"x-appwrite": {
"method": "updateSmtpProvider",
"weight": 367,
"weight": 361,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15538,7 +15540,7 @@
},
"x-appwrite": {
"method": "createTelesignProvider",
"weight": 356,
"weight": 350,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15644,7 +15646,7 @@
},
"x-appwrite": {
"method": "updateTelesignProvider",
"weight": 369,
"weight": 363,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15748,7 +15750,7 @@
},
"x-appwrite": {
"method": "createTextmagicProvider",
"weight": 357,
"weight": 351,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15854,7 +15856,7 @@
},
"x-appwrite": {
"method": "updateTextmagicProvider",
"weight": 370,
"weight": 364,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15958,7 +15960,7 @@
},
"x-appwrite": {
"method": "createTwilioProvider",
"weight": 358,
"weight": 352,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16064,7 +16066,7 @@
},
"x-appwrite": {
"method": "updateTwilioProvider",
"weight": 371,
"weight": 365,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16168,7 +16170,7 @@
},
"x-appwrite": {
"method": "createVonageProvider",
"weight": 359,
"weight": 353,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16274,7 +16276,7 @@
},
"x-appwrite": {
"method": "updateVonageProvider",
"weight": 372,
"weight": 366,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16378,7 +16380,7 @@
},
"x-appwrite": {
"method": "getProvider",
"weight": 364,
"weight": 358,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16438,7 +16440,7 @@
},
"x-appwrite": {
"method": "deleteProvider",
"weight": 375,
"weight": 369,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16503,7 +16505,7 @@
},
"x-appwrite": {
"method": "listProviderLogs",
"weight": 363,
"weight": 357,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16580,7 +16582,7 @@
},
"x-appwrite": {
"method": "listSubscriberLogs",
"weight": 384,
"weight": 378,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16657,7 +16659,7 @@
},
"x-appwrite": {
"method": "listTopics",
"weight": 377,
"weight": 371,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16733,7 +16735,7 @@
},
"x-appwrite": {
"method": "createTopic",
"weight": 376,
"weight": 370,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16826,7 +16828,7 @@
},
"x-appwrite": {
"method": "getTopic",
"weight": 379,
"weight": 373,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16889,7 +16891,7 @@
},
"x-appwrite": {
"method": "updateTopic",
"weight": 380,
"weight": 374,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16973,7 +16975,7 @@
},
"x-appwrite": {
"method": "deleteTopic",
"weight": 381,
"weight": 375,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17038,7 +17040,7 @@
},
"x-appwrite": {
"method": "listTopicLogs",
"weight": 378,
"weight": 372,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17115,7 +17117,7 @@
},
"x-appwrite": {
"method": "listSubscribers",
"weight": 383,
"weight": 377,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17199,7 +17201,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 382,
"weight": 376,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17293,7 +17295,7 @@
},
"x-appwrite": {
"method": "getSubscriber",
"weight": 385,
"weight": 379,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17361,7 +17363,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 386,
"weight": 380,
"cookies": false,
"type": "",
"deprecated": false,
@@ -26233,7 +26235,7 @@
},
"schedule": {
"type": "string",
"description": "Function execution schedult in CRON format.",
"description": "Function execution schedule in CRON format.",
"x-example": "5 4 * * *"
},
"timeout": {
+15 -15
View File
@@ -61,9 +61,9 @@ $avatarCallback = function (string $type, string $code, int $width, int $height,
unset($image);
};
$getUserGitHub = function (string $userId, Document $project, Database $dbForProject, Database $dbForConsole, ?Logger $logger) {
$getUserGitHub = function (string $userId, Document $project, Database $dbForProject, Database $dbForPlatform, ?Logger $logger) {
try {
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
$sessions = $user->getAttribute('sessions', []);
@@ -122,7 +122,7 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro
do {
$previousAccessToken = $gitHubSession->getAttribute('providerAccessToken');
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
$sessions = $user->getAttribute('sessions', []);
$gitHubSession = new Document();
@@ -565,14 +565,14 @@ App::get('/v1/cards/cloud')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('response')
->inject('heroes')
->inject('contributors')
->inject('employees')
->inject('logger')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
if ($user->isEmpty() && empty($mock)) {
throw new Exception(Exception::USER_NOT_FOUND);
@@ -583,7 +583,7 @@ App::get('/v1/cards/cloud')
$email = $user->getAttribute('email', '');
$createdAt = new \DateTime($user->getCreatedAt());
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger);
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger);
$githubName = $gitHub['name'] ?? '';
$githubId = $gitHub['id'] ?? '';
@@ -772,14 +772,14 @@ App::get('/v1/cards/cloud-back')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('response')
->inject('heroes')
->inject('contributors')
->inject('employees')
->inject('logger')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
if ($user->isEmpty() && empty($mock)) {
throw new Exception(Exception::USER_NOT_FOUND);
@@ -789,7 +789,7 @@ App::get('/v1/cards/cloud-back')
$userId = $user->getId();
$email = $user->getAttribute('email', '');
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger);
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger);
$githubId = $gitHub['id'] ?? '';
$isHero = \array_key_exists($email, $heroes);
@@ -850,14 +850,14 @@ App::get('/v1/cards/cloud-og')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('response')
->inject('heroes')
->inject('contributors')
->inject('employees')
->inject('logger')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
if ($user->isEmpty() && empty($mock)) {
throw new Exception(Exception::USER_NOT_FOUND);
@@ -872,7 +872,7 @@ App::get('/v1/cards/cloud-og')
$email = $user->getAttribute('email', '');
$createdAt = new \DateTime($user->getCreatedAt());
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger);
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger);
$githubName = $gitHub['name'] ?? '';
$githubId = $gitHub['id'] ?? '';
+1 -1
View File
@@ -71,7 +71,7 @@ App::post('/v1/console/assistant')
->param('prompt', '', new Text(2000), 'Prompt. A string containing questions asked to the AI assistant.')
->inject('response')
->action(function (string $prompt, Response $response) {
$ch = curl_init('http://appwrite-assistant:3003/');
$ch = curl_init('http://appwrite-assistant:3003/v1/models/assistant/prompt');
$responseHeaders = [];
$query = json_encode(['prompt' => $prompt]);
$headers = ['accept: text/event-stream'];
+100 -40
View File
@@ -19,7 +19,6 @@ use Utopia\Audit\Audit;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception as DatabaseException;
use Utopia\Database\Exception\Authorization as AuthorizationException;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Duplicate as DuplicateException;
@@ -152,7 +151,7 @@ function createAttribute(string $databaseId, string $collectionId, Document $att
} catch (DuplicateException) {
throw new Exception(Exception::ATTRIBUTE_ALREADY_EXISTS);
} catch (LimitException) {
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, 'Attribute limit exceeded');
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED);
} catch (\Throwable $e) {
$dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId);
$dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $collection->getInternalId());
@@ -196,7 +195,7 @@ function createAttribute(string $databaseId, string $collectionId, Document $att
throw new Exception(Exception::ATTRIBUTE_ALREADY_EXISTS);
} catch (LimitException) {
$dbForProject->deleteDocument('attributes', $attribute->getId());
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, 'Attribute limit exceeded');
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED);
} catch (\Throwable $e) {
$dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $relatedCollection->getId());
$dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedCollection->getInternalId());
@@ -290,15 +289,9 @@ function updateAttribute(
$attribute->setAttribute('size', $size);
}
$formatOptions = $attribute->getAttribute('formatOptions');
switch ($attribute->getAttribute('format')) {
case APP_DATABASE_ATTRIBUTE_INT_RANGE:
case APP_DATABASE_ATTRIBUTE_FLOAT_RANGE:
if ($min === $formatOptions['min'] && $max === $formatOptions['max']) {
break;
}
if ($min > $max) {
throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Minimum value must be lesser than maximum value');
}
@@ -385,30 +378,42 @@ function updateAttribute(
size: $size,
required: $required,
default: $default,
formatOptions: $options ?? null,
formatOptions: $options,
newKey: $newKey ?? null
);
} catch (TruncateException) {
throw new Exception(Exception::ATTRIBUTE_INVALID_RESIZE);
} catch (NotFoundException) {
throw new Exception(Exception::ATTRIBUTE_NOT_FOUND);
} catch (LimitException) {
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED);
}
}
if (!empty($newKey) && $key !== $newKey) {
// Delete attribute and recreate since we can't modify IDs
$original = clone $attribute;
$dbForProject->deleteDocument('attributes', $attribute->getId());
$originalUid = $attribute->getId();
$attribute
->setAttribute('$id', ID::custom($db->getInternalId() . '_' . $collection->getInternalId() . '_' . $newKey))
->setAttribute('key', $newKey);
try {
$attribute = $dbForProject->createDocument('attributes', $attribute);
} catch (DatabaseException|PDOException) {
$attribute = $dbForProject->createDocument('attributes', $original);
$dbForProject->updateDocument('attributes', $originalUid, $attribute);
/**
* @var Document $index
*/
foreach ($collection->getAttribute('indexes') as $index) {
/**
* @var string[] $attributes
*/
$attributes = $index->getAttribute('attributes', []);
$found = \array_search($key, $attributes);
if ($found !== false) {
$attributes[$found] = $newKey;
$index->setAttribute('attributes', $attributes);
$dbForProject->updateDocument('indexes', $index->getId(), $index);
}
}
} else {
$attribute = $dbForProject->updateDocument('attributes', $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key, $attribute);
@@ -2572,7 +2577,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
$attributeStatus = $oldAttributes[$attributeIndex]['status'];
$attributeType = $oldAttributes[$attributeIndex]['type'];
$attributeSize = $oldAttributes[$attributeIndex]['size'];
$attributeArray = $oldAttributes[$attributeIndex]['array'] ?? false;
if ($attributeType === Database::VAR_RELATIONSHIP) {
@@ -2586,10 +2590,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
$lengths[$i] = null;
if ($attributeType === Database::VAR_STRING) {
$lengths[$i] = $attributeSize; // set attribute size as index length only for strings
}
if ($attributeArray === true) {
$lengths[$i] = Database::ARRAY_INDEX_LENGTH;
$orders[$i] = null;
@@ -2612,7 +2612,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
$validator = new IndexValidator(
$collection->getAttribute('attributes'),
$dbForProject->getAdapter()->getMaxIndexLength()
$dbForProject->getAdapter()->getMaxIndexLength(),
$dbForProject->getAdapter()->getInternalIndexesKeys(),
);
if (!$validator->isValid($index)) {
throw new Exception(Exception::INDEX_INVALID, $validator->getDescription());
@@ -2928,7 +2929,11 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
$data['$permissions'] = $permissions;
$document = new Document($data);
$checkPermissions = function (Document $collection, Document $document, string $permission) use (&$checkPermissions, $dbForProject, $database) {
$operations = 0;
$checkPermissions = function (Document $collection, Document $document, string $permission) use (&$checkPermissions, $dbForProject, $database, &$operations) {
$operations++;
$documentSecurity = $collection->getAttribute('documentSecurity', false);
$validator = new Authorization($permission);
@@ -3020,6 +3025,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
// Add $collectionId and $databaseId for all documents
$processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) {
$document->setAttribute('$databaseId', $database->getId());
@@ -3055,6 +3061,13 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
$processDocument($collection, $document);
$queueForUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, $operations)
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations)
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getInternalId(), $collection->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_STORAGE), 1); // per collection
$response->addHeader('X-Debug-Operations', $operations);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($document, Response::MODEL_DOCUMENT);
@@ -3074,10 +3087,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
->setContext('collection', $collection)
->setContext('database', $database)
->setPayload($response->getPayload(), sensitive: $relationships);
$queueForUsage
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getInternalId(), $collection->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_STORAGE), 1); // per collection
});
App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
@@ -3100,7 +3109,8 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
->inject('response')
->inject('dbForProject')
->inject('mode')
->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, string $mode) {
->inject('queueForUsage')
->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, string $mode, Usage $queueForUsage) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
@@ -3136,7 +3146,6 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$documentId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId));
@@ -3151,12 +3160,16 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
$documents = $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries);
$total = $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries, APP_LIMIT_COUNT);
$operations = 0;
// Add $collectionId and $databaseId for all documents
$processDocument = (function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database): bool {
$processDocument = (function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, &$operations): bool {
if ($document->isEmpty()) {
return false;
}
$operations++;
$document->removeAttribute('$collection');
$document->setAttribute('$databaseId', $database->getId());
$document->setAttribute('$collectionId', $collection->getId());
@@ -3170,8 +3183,13 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
$related = $document->getAttribute($relationship->getAttribute('key'));
if (empty($related)) {
if (\in_array(\gettype($related), ['array', 'object'])) {
$operations++;
}
continue;
}
if (!\is_array($related)) {
$relations = [$related];
} else {
@@ -3179,6 +3197,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
}
$relatedCollectionId = $relationship->getAttribute('relatedCollection');
// todo: Use local cache for this getDocument
$relatedCollection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId));
foreach ($relations as $index => $doc) {
@@ -3203,6 +3222,13 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
$processDocument($collection, $document);
}
$queueForUsage
->addMetric(METRIC_DATABASES_OPERATIONS_READS, $operations)
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations)
;
$response->addHeader('X-Debug-Operations', $operations);
$select = \array_reduce($queries, function ($result, $query) {
return $result || ($query->getMethod() === Query::TYPE_SELECT);
}, false);
@@ -3258,9 +3284,9 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
->inject('response')
->inject('dbForProject')
->inject('mode')
->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, string $mode) {
->inject('queueForUsage')
->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, string $mode, Usage $queueForUsage) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
@@ -3287,12 +3313,16 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
throw new Exception(Exception::DOCUMENT_NOT_FOUND);
}
$operations = 0;
// Add $collectionId and $databaseId for all documents
$processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) {
$processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, &$operations) {
if ($document->isEmpty()) {
return;
}
$operations++;
$document->setAttribute('$databaseId', $database->getId());
$document->setAttribute('$collectionId', $collection->getId());
@@ -3305,8 +3335,13 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
$related = $document->getAttribute($relationship->getAttribute('key'));
if (empty($related)) {
if (\in_array(\gettype($related), ['array', 'object'])) {
$operations++;
}
continue;
}
if (!\is_array($related)) {
$related = [$related];
}
@@ -3326,6 +3361,13 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
$processDocument($collection, $document);
$queueForUsage
->addMetric(METRIC_DATABASES_OPERATIONS_READS, $operations)
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations)
;
$response->addHeader('X-Debug-Operations', $operations);
$response->dynamic($document, Response::MODEL_DOCUMENT);
});
@@ -3421,6 +3463,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
$output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($geoRecord['countryCode']), false) ? \strtolower($geoRecord['countryCode']) : '--';
$output[$i]['countryName'] = $locale->getText('countries.' . strtolower($geoRecord['countryCode']), $locale->getText('locale.country.unknown'));
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource),
'logs' => $output,
@@ -3458,7 +3501,8 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
->inject('dbForProject')
->inject('queueForEvents')
->inject('mode')
->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, string $mode) {
->inject('queueForUsage')
->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Usage $queueForUsage) {
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
@@ -3525,7 +3569,12 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
$data['$permissions'] = $permissions;
$newDocument = new Document($data);
$setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database) {
$operations = 0;
$setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database, &$operations) {
$operations++;
$relationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
@@ -3593,6 +3642,13 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
$setCollection($collection, $newDocument);
$queueForUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, $operations)
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations)
;
$response->addHeader('X-Debug-Operations', $operations);
try {
$document = $dbForProject->withRequestTimestamp(
$requestTimestamp,
@@ -3764,6 +3820,13 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
$processDocument($collection, $document);
$queueForUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, 1)
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_WRITES), 1)
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getInternalId(), $collection->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_STORAGE), 1); // per collection
$response->addHeader('X-Debug-Operations', 1);
$relationships = \array_map(
fn ($document) => $document->getAttribute('key'),
\array_filter(
@@ -3780,9 +3843,6 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
->setContext('database', $database)
->setPayload($response->output($document, Response::MODEL_DOCUMENT), sensitive: $relationships);
$queueForUsage
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getInternalId(), $collection->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_STORAGE), 1); // per collection
$response->noContent();
});
+74 -42
View File
@@ -22,6 +22,7 @@ use Appwrite\Utopia\Database\Validator\Queries\Functions;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Rule;
use Executor\Executor;
use Utopia\Abuse\Abuse;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Config;
@@ -177,15 +178,45 @@ App::post('/v1/functions')
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('timelimit')
->inject('project')
->inject('user')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('gitHub')
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) {
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, callable $timelimit, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForPlatform, GitHub $github) use ($redeployVcs) {
$functionId = ($functionId == 'unique()') ? ID::unique() : $functionId;
// Temporary abuse check
$abuseCheck = function () use ($project, $timelimit, $response) {
$abuseKey = "projectId:{projectId},url:{url}";
$abuseLimit = App::getEnv('_APP_FUNCTIONS_CREATION_ABUSE_LIMIT', 50);
$abuseTime = 86400; // 1 day
$timeLimit = $timelimit($abuseKey, $abuseLimit, $abuseTime);
$timeLimit
->setParam('{projectId}', $project->getId())
->setParam('{url}', '/v1/functions');
$abuse = new Abuse($timeLimit);
$remaining = $timeLimit->remaining();
$limit = $timeLimit->limit();
$time = $timeLimit->time() + $abuseTime;
$response
->addHeader('X-RateLimit-Limit', $limit)
->addHeader('X-RateLimit-Remaining', $remaining)
->addHeader('X-RateLimit-Reset', $time);
$enabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled';
if ($enabled && $abuse->check()) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED);
}
};
$abuseCheck();
$allowList = \array_filter(\explode(',', System::getEnv('_APP_FUNCTIONS_RUNTIMES', '')));
if (!empty($allowList) && !\in_array($runtime, $allowList)) {
@@ -206,7 +237,7 @@ App::post('/v1/functions')
->setAttribute('version', $templateVersion);
}
$installation = $dbForConsole->getDocument('installations', $installationId);
$installation = $dbForPlatform->getDocument('installations', $installationId);
if (!empty($installationId) && $installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -248,7 +279,7 @@ App::post('/v1/functions')
]));
$schedule = Authorization::skip(
fn () => $dbForConsole->createDocument('schedules', new Document([
fn () => $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'), // Todo replace with projects region
'resourceType' => 'function',
'resourceId' => $function->getId(),
@@ -267,7 +298,7 @@ App::post('/v1/functions')
if (!empty($providerRepositoryId)) {
$teamId = $project->getAttribute('teamId', '');
$repository = $dbForConsole->createDocument('repositories', new Document([
$repository = $dbForPlatform->createDocument('repositories', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
@@ -327,10 +358,11 @@ App::post('/v1/functions')
if (!empty($functionsDomain)) {
$routeSubdomain = ID::unique();
$domain = "{$routeSubdomain}.{$functionsDomain}";
$ruleId = md5($domain);
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique();
$rule = Authorization::skip(
fn () => $dbForConsole->createDocument('rules', new Document([
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
@@ -802,9 +834,9 @@ App::put('/v1/functions/:functionId')
->inject('project')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('gitHub')
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) {
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForPlatform, GitHub $github) use ($redeployVcs) {
// TODO: If only branch changes, re-deploy
$function = $dbForProject->getDocument('functions', $functionId);
@@ -812,7 +844,7 @@ App::put('/v1/functions/:functionId')
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$installation = $dbForConsole->getDocument('installations', $installationId);
$installation = $dbForPlatform->getDocument('installations', $installationId);
if (!empty($installationId) && $installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -843,7 +875,7 @@ App::put('/v1/functions/:functionId')
// Git disconnect logic. Disconnecting only when providerRepositoryId is empty, allowing for continue updates without disconnecting git
if ($isConnected && ($providerRepositoryId !== null && empty($providerRepositoryId))) {
$repositories = $dbForConsole->find('repositories', [
$repositories = $dbForPlatform->find('repositories', [
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::equal('resourceInternalId', [$function->getInternalId()]),
Query::equal('resourceType', ['function']),
@@ -851,7 +883,7 @@ App::put('/v1/functions/:functionId')
]);
foreach ($repositories as $repository) {
$dbForConsole->deleteDocument('repositories', $repository->getId());
$dbForPlatform->deleteDocument('repositories', $repository->getId());
}
$providerRepositoryId = '';
@@ -867,7 +899,7 @@ App::put('/v1/functions/:functionId')
if (!$isConnected && !empty($providerRepositoryId)) {
$teamId = $project->getAttribute('teamId', '');
$repository = $dbForConsole->createDocument('repositories', new Document([
$repository = $dbForPlatform->createDocument('repositories', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
@@ -949,12 +981,12 @@ App::put('/v1/functions/:functionId')
}
// Inform scheduler if function is still active
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$queueForEvents->setParam('functionId', $function->getId());
@@ -1067,8 +1099,8 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('dbForConsole')
->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForPlatform) {
$function = $dbForProject->getDocument('functions', $functionId);
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
@@ -1096,12 +1128,12 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId')
])));
// Inform scheduler if function is still active
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$queueForEvents
->setParam('functionId', $function->getId())
@@ -1129,8 +1161,8 @@ App::delete('/v1/functions/:functionId')
->inject('dbForProject')
->inject('queueForDeletes')
->inject('queueForEvents')
->inject('dbForConsole')
->action(function (string $functionId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $functionId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Database $dbForPlatform) {
$function = $dbForProject->getDocument('functions', $functionId);
@@ -1143,11 +1175,11 @@ App::delete('/v1/functions/:functionId')
}
// Inform scheduler to no longer run function
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('active', false);
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
@@ -1757,13 +1789,13 @@ App::post('/v1/functions/:functionId/executions')
->inject('request')
->inject('project')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('user')
->inject('queueForEvents')
->inject('queueForUsage')
->inject('queueForFunctions')
->inject('geoRecord')
->action(function (string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForConsole, Document $user, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, array $geoRecord) {
->action(function (string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForPlatform, Document $user, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, array $geoRecord) {
$async = \strval($async) === 'true' || \strval($async) === '1';
if (!$async && !is_null($scheduledAt)) {
@@ -1942,7 +1974,7 @@ App::post('/v1/functions/:functionId/executions')
'userId' => $user->getId()
];
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => ScheduleExecutions::getSupportedResource(),
'resourceId' => $execution->getId(),
@@ -2277,9 +2309,9 @@ App::delete('/v1/functions/:functionId/executions/:executionId')
->param('executionId', '', new UID(), 'Execution ID.')
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForEvents')
->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, Database $dbForConsole, Event $queueForEvents) {
->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@@ -2305,7 +2337,7 @@ App::delete('/v1/functions/:functionId/executions/:executionId')
}
if ($status === 'scheduled') {
$schedule = $dbForConsole->findOne('schedules', [
$schedule = $dbForPlatform->findOne('schedules', [
Query::equal('resourceId', [$execution->getId()]),
Query::equal('resourceType', [ScheduleExecutions::getSupportedResource()]),
Query::equal('active', [true]),
@@ -2316,7 +2348,7 @@ App::delete('/v1/functions/:functionId/executions/:executionId')
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('active', false);
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
}
}
@@ -2349,8 +2381,8 @@ App::post('/v1/functions/:functionId/variables')
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (string $functionId, string $key, string $value, Response $response, Database $dbForProject, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $functionId, string $key, string $value, Response $response, Database $dbForProject, Database $dbForPlatform) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@@ -2383,12 +2415,12 @@ App::post('/v1/functions/:functionId/variables')
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
// Inform scheduler to pull the latest changes
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -2483,8 +2515,8 @@ App::put('/v1/functions/:functionId/variables/:variableId')
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (string $functionId, string $variableId, string $key, ?string $value, Response $response, Database $dbForProject, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $functionId, string $variableId, string $key, ?string $value, Response $response, Database $dbForProject, Database $dbForPlatform) {
$function = $dbForProject->getDocument('functions', $functionId);
@@ -2515,12 +2547,12 @@ App::put('/v1/functions/:functionId/variables/:variableId')
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
// Inform scheduler to pull the latest changes
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$response->dynamic($variable, Response::MODEL_VARIABLE);
});
@@ -2542,8 +2574,8 @@ App::delete('/v1/functions/:functionId/variables/:variableId')
->param('variableId', '', new UID(), 'Variable unique ID.', false)
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (string $functionId, string $variableId, Response $response, Database $dbForProject, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $functionId, string $variableId, Response $response, Database $dbForProject, Database $dbForPlatform) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@@ -2564,12 +2596,12 @@ App::delete('/v1/functions/:functionId/variables/:variableId')
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
// Inform scheduler to pull the latest changes
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$response->noContent();
});
+27 -27
View File
@@ -2627,11 +2627,11 @@ App::post('/v1/messaging/messages/email')
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, array $cc, array $bcc, array $attachments, bool $draft, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, array $cc, array $bcc, array $attachments, bool $draft, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -2720,7 +2720,7 @@ App::post('/v1/messaging/messages/email')
->setMessageId($message->getId());
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => 'message',
'resourceId' => $message->getId(),
@@ -2775,11 +2775,11 @@ App::post('/v1/messaging/messages/sms')
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, string $content, array $topics, array $users, array $targets, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, string $content, array $topics, array $users, array $targets, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -2837,7 +2837,7 @@ App::post('/v1/messaging/messages/sms')
->setMessageId($message->getId());
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => 'message',
'resourceId' => $message->getId(),
@@ -2901,11 +2901,11 @@ App::post('/v1/messaging/messages/push')
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, string $title, string $body, array $topics, array $users, array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, string $badge, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, string $title, string $body, array $topics, array $users, array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, string $badge, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -3014,7 +3014,7 @@ App::post('/v1/messaging/messages/push')
->setMessageId($message->getId());
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => 'message',
'resourceId' => $message->getId(),
@@ -3310,11 +3310,11 @@ App::patch('/v1/messaging/messages/email/:messageId')
->param('attachments', null, new ArrayList(new CompoundUID()), 'Array of compound ID strings of bucket IDs and file IDs to be attached to the email. They should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $subject, ?string $content, ?bool $draft, ?bool $html, ?array $cc, ?array $bcc, ?string $scheduledAt, ?array $attachments, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $subject, ?string $content, ?bool $draft, ?bool $html, ?array $cc, ?array $bcc, ?string $scheduledAt, ?array $attachments, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -3366,7 +3366,7 @@ App::patch('/v1/messaging/messages/email/:messageId')
}
if (\is_null($currentScheduledAt) && !\is_null($scheduledAt)) {
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => 'message',
'resourceId' => $message->getId(),
@@ -3381,7 +3381,7 @@ App::patch('/v1/messaging/messages/email/:messageId')
}
if (!\is_null($currentScheduledAt)) {
$schedule = $dbForConsole->getDocument('schedules', $message->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $message->getAttribute('scheduleId'));
$scheduledStatus = ($status ?? $message->getAttribute('status')) === MessageStatus::SCHEDULED;
if ($schedule->isEmpty()) {
@@ -3396,7 +3396,7 @@ App::patch('/v1/messaging/messages/email/:messageId')
$schedule->setAttribute('schedule', $scheduledAt);
}
$dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule);
$dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule);
}
if (!\is_null($scheduledAt)) {
@@ -3506,11 +3506,11 @@ App::patch('/v1/messaging/messages/sms/:messageId')
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $content, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $content, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -3562,7 +3562,7 @@ App::patch('/v1/messaging/messages/sms/:messageId')
}
if (\is_null($currentScheduledAt) && !\is_null($scheduledAt)) {
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => 'message',
'resourceId' => $message->getId(),
@@ -3577,7 +3577,7 @@ App::patch('/v1/messaging/messages/sms/:messageId')
}
if (!\is_null($currentScheduledAt)) {
$schedule = $dbForConsole->getDocument('schedules', $message->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $message->getAttribute('scheduleId'));
$scheduledStatus = ($status ?? $message->getAttribute('status')) === MessageStatus::SCHEDULED;
if ($schedule->isEmpty()) {
@@ -3592,7 +3592,7 @@ App::patch('/v1/messaging/messages/sms/:messageId')
$schedule->setAttribute('schedule', $scheduledAt);
}
$dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule);
$dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule);
}
if (!\is_null($scheduledAt)) {
@@ -3671,11 +3671,11 @@ App::patch('/v1/messaging/messages/push/:messageId')
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -3727,7 +3727,7 @@ App::patch('/v1/messaging/messages/push/:messageId')
}
if (\is_null($currentScheduledAt) && !\is_null($scheduledAt)) {
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => 'message',
'resourceId' => $message->getId(),
@@ -3742,7 +3742,7 @@ App::patch('/v1/messaging/messages/push/:messageId')
}
if (!\is_null($currentScheduledAt)) {
$schedule = $dbForConsole->getDocument('schedules', $message->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $message->getAttribute('scheduleId'));
$scheduledStatus = ($status ?? $message->getAttribute('status')) === MessageStatus::SCHEDULED;
if ($schedule->isEmpty()) {
@@ -3757,7 +3757,7 @@ App::patch('/v1/messaging/messages/push/:messageId')
$schedule->setAttribute('schedule', $scheduledAt);
}
$dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule);
$dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule);
}
if (!\is_null($scheduledAt)) {
@@ -3894,10 +3894,10 @@ App::delete('/v1/messaging/messages/:messageId')
->label('sdk.response.model', Response::MODEL_NONE)
->param('messageId', '', new UID(), 'Message ID.')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForEvents')
->inject('response')
->action(function (string $messageId, Database $dbForProject, Database $dbForConsole, Event $queueForEvents, Response $response) {
->action(function (string $messageId, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents, Response $response) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -3920,7 +3920,7 @@ App::delete('/v1/messaging/messages/:messageId')
if (!empty($scheduleId)) {
try {
$dbForConsole->deleteDocument('schedules', $scheduleId);
$dbForPlatform->deleteDocument('schedules', $scheduleId);
} catch (Exception) {
// Ignore
}
+1 -473
View File
@@ -1,17 +1,12 @@
<?php
use Appwrite\Auth\OAuth2\Firebase as OAuth2Firebase;
use Appwrite\Event\Event;
use Appwrite\Event\Migration;
use Appwrite\Extend\Exception;
use Appwrite\Permission;
use Appwrite\Role;
use Appwrite\Utopia\Database\Validator\Queries\Migrations;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
@@ -22,9 +17,7 @@ use Utopia\Migration\Sources\Appwrite;
use Utopia\Migration\Sources\Firebase;
use Utopia\Migration\Sources\NHost;
use Utopia\Migration\Sources\Supabase;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Host;
use Utopia\Validator\Integer;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
@@ -87,112 +80,9 @@ App::post('/v1/migrations/appwrite')
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::post('/v1/migrations/firebase/oauth')
->groups(['api', 'migrations'])
->desc('Migrate Firebase data (OAuth)')
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].create')
->label('audits.event', 'migration.create')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'createFirebaseOAuthMigration')
->label('sdk.description', '/docs/references/migrations/migration-firebase.md')
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MIGRATION)
->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate')
->param('projectId', '', new Text(65536), 'Project ID of the Firebase Project')
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->inject('project')
->inject('user')
->inject('queueForEvents')
->inject('queueForMigrations')
->inject('request')
->action(function (array $resources, string $projectId, Response $response, Database $dbForProject, Database $dbForConsole, Document $project, Document $user, Event $queueForEvents, Migration $queueForMigrations, Request $request) {
$firebase = new OAuth2Firebase(
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
);
$identity = $dbForConsole->findOne('identities', [
Query::equal('provider', ['firebase']),
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity->isEmpty()) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$accessToken = $identity->getAttribute('providerAccessToken');
$refreshToken = $identity->getAttribute('providerRefreshToken');
$accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry');
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
if ($isExpired) {
$firebase->refreshTokens($refreshToken);
$accessToken = $firebase->getAccessToken('');
$refreshToken = $firebase->getRefreshToken('');
$verificationId = $firebase->getUserID($accessToken);
if (empty($verificationId)) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.');
}
$identity = $identity
->setAttribute('providerAccessToken', $accessToken)
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry('')));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
}
if ($identity->getAttribute('secrets')) {
$serviceAccount = $identity->getAttribute('secrets');
} else {
$firebase->cleanupServiceAccounts($accessToken, $projectId);
$serviceAccount = $firebase->createServiceAccount($accessToken, $projectId);
$identity = $identity
->setAttribute('secrets', json_encode($serviceAccount));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
}
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
'stage' => 'init',
'source' => Firebase::getName(),
'destination' => Appwrite::getName(),
'credentials' => [
'serviceAccount' => json_encode($serviceAccount),
],
'resources' => $resources,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => []
]));
$queueForEvents->setParam('migrationId', $migration->getId());
// Trigger Transfer
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setUser($user)
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::post('/v1/migrations/firebase')
->groups(['api', 'migrations'])
->desc('Migrate Firebase data (Service Account)')
->desc('Migrate Firebase data')
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].create')
->label('audits.event', 'migration.create')
@@ -547,368 +437,6 @@ App::get('/v1/migrations/firebase/report')
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
});
App::get('/v1/migrations/firebase/report/oauth')
->groups(['api', 'migrations'])
->desc('Generate a report on Firebase data using OAuth')
->label('scope', 'migrations.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'getFirebaseReportOAuth')
->label('sdk.description', '/docs/references/migrations/migration-firebase-report.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MIGRATION_REPORT)
->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate')
->param('projectId', '', new Text(65536), 'Project ID')
->inject('response')
->inject('request')
->inject('user')
->inject('dbForConsole')
->action(function (array $resources, string $projectId, Response $response, Request $request, Document $user, Database $dbForConsole) {
$firebase = new OAuth2Firebase(
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
);
$identity = $dbForConsole->findOne('identities', [
Query::equal('provider', ['firebase']),
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity->isEmpty()) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$accessToken = $identity->getAttribute('providerAccessToken');
$refreshToken = $identity->getAttribute('providerRefreshToken');
$accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry');
if (empty($accessToken) || empty($refreshToken) || empty($accessTokenExpiry)) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
if (System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', '') === '' || System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', '') === '') {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
if ($isExpired) {
$firebase->refreshTokens($refreshToken);
$accessToken = $firebase->getAccessToken('');
$refreshToken = $firebase->getRefreshToken('');
$verificationId = $firebase->getUserID($accessToken);
if (empty($verificationId)) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.');
}
$identity = $identity
->setAttribute('providerAccessToken', $accessToken)
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry('')));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
}
// Get Service Account
if ($identity->getAttribute('secrets')) {
$serviceAccount = $identity->getAttribute('secrets');
} else {
$firebase->cleanupServiceAccounts($accessToken, $projectId);
$serviceAccount = $firebase->createServiceAccount($accessToken, $projectId);
$identity = $identity
->setAttribute('secrets', json_encode($serviceAccount));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
}
$firebase = new Firebase($serviceAccount);
try {
$report = $firebase->report($resources);
} catch (\Throwable $e) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Source Error: ' . $e->getMessage());
}
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
});
App::get('/v1/migrations/firebase/connect')
->desc('Authorize with Firebase')
->groups(['api', 'migrations'])
->label('scope', 'migrations.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'createFirebaseAuth')
->label('sdk.description', '')
->label('sdk.response.code', Response::STATUS_CODE_MOVED_PERMANENTLY)
->label('sdk.response.type', Response::CONTENT_TYPE_HTML)
->label('sdk.methodType', 'webAuth')
->label('sdk.hide', true)
->param('redirect', '', fn ($clients) => new Host($clients), 'URL to redirect back to your Firebase authorization. Only console hostnames are allowed.', true, ['clients'])
->param('projectId', '', new UID(), 'Project ID')
->inject('response')
->inject('request')
->inject('user')
->inject('dbForConsole')
->action(function (string $redirect, string $projectId, Response $response, Request $request, Document $user, Database $dbForConsole) {
$state = \json_encode([
'projectId' => $projectId,
'redirect' => $redirect,
]);
$prefs = $user->getAttribute('prefs', []);
$prefs['migrationState'] = $state;
$user->setAttribute('prefs', $prefs);
$dbForConsole->updateDocument('users', $user->getId(), $user);
$oauth2 = new OAuth2Firebase(
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
);
$url = $oauth2->getLoginURL();
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($url);
});
App::get('/v1/migrations/firebase/redirect')
->desc('Capture and receive data on Firebase authorization')
->groups(['api', 'migrations'])
->label('scope', 'public')
->label('error', __DIR__ . '/../../views/general/error.phtml')
->param('code', '', new Text(2048), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true)
->inject('user')
->inject('project')
->inject('request')
->inject('response')
->inject('dbForConsole')
->action(function (string $code, Document $user, Document $project, Request $request, Response $response, Database $dbForConsole) {
$state = $user['prefs']['migrationState'] ?? '{}';
$prefs['migrationState'] = '';
$user->setAttribute('prefs', $prefs);
$dbForConsole->updateDocument('users', $user->getId(), $user);
if (empty($state)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Installation requests from organisation members for the Appwrite Google App are currently unsupported.');
}
$state = \json_decode($state, true);
$redirect = $state['redirect'] ?? '';
$projectId = $state['projectId'] ?? '';
$project = $dbForConsole->getDocument('projects', $projectId);
if (empty($redirect)) {
$redirect = $request->getProtocol() . '://' . $request->getHostname() . '/console/project-$projectId/settings/migrations';
}
if ($project->isEmpty()) {
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirect);
return;
}
// OAuth Authroization
if (!empty($code)) {
$oauth2 = new OAuth2Firebase(
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
);
$accessToken = $oauth2->getAccessToken($code);
$refreshToken = $oauth2->getRefreshToken($code);
$accessTokenExpiry = $oauth2->getAccessTokenExpiry($code);
$email = $oauth2->getUserEmail($accessToken);
$oauth2ID = $oauth2->getUserID($accessToken);
if (empty($accessToken)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to get access token.');
}
if (empty($refreshToken)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to get refresh token.');
}
if (empty($accessTokenExpiry)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to get access token expiry.');
}
// Makes sure this email is not already used in another identity
$identity = $dbForConsole->findOne('identities', [
Query::equal('providerEmail', [$email]),
]);
if (!$identity->isEmpty()) {
if ($identity->getAttribute('userInternalId', '') !== $user->getInternalId()) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
}
if (!$identity->isEmpty()) {
$identity = $identity
->setAttribute('providerAccessToken', $accessToken)
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
} else {
$identity = $dbForConsole->createDocument('identities', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
],
'userInternalId' => $user->getInternalId(),
'userId' => $user->getId(),
'provider' => 'firebase',
'providerUid' => $oauth2ID,
'providerEmail' => $email,
'providerAccessToken' => $accessToken,
'providerRefreshToken' => $refreshToken,
'providerAccessTokenExpiry' => DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry),
]));
}
} else {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Missing OAuth2 code.');
}
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirect);
});
App::get('/v1/migrations/firebase/projects')
->desc('List Firebase projects')
->groups(['api', 'migrations'])
->label('scope', 'migrations.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'listFirebaseProjects')
->label('sdk.description', '')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST)
->inject('user')
->inject('response')
->inject('project')
->inject('dbForConsole')
->inject('request')
->action(function (Document $user, Response $response, Document $project, Database $dbForConsole, Request $request) {
$firebase = new OAuth2Firebase(
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
);
$identity = $dbForConsole->findOne('identities', [
Query::equal('provider', ['firebase']),
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity->isEmpty()) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$accessToken = $identity->getAttribute('providerAccessToken');
$refreshToken = $identity->getAttribute('providerRefreshToken');
$accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry');
if (empty($accessToken) || empty($refreshToken) || empty($accessTokenExpiry)) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
if (System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', '') === '' || System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', '') === '') {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
try {
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
if ($isExpired) {
try {
$firebase->refreshTokens($refreshToken);
} catch (\Throwable $e) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$accessToken = $firebase->getAccessToken('');
$refreshToken = $firebase->getRefreshToken('');
$verificationId = $firebase->getUserID($accessToken);
if (empty($verificationId)) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.');
}
$identity = $identity
->setAttribute('providerAccessToken', $accessToken)
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry('')));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
}
$projects = $firebase->getProjects($accessToken);
$output = [];
foreach ($projects as $project) {
$output[] = [
'displayName' => $project['displayName'],
'projectId' => $project['projectId'],
];
}
} catch (\Throwable $e) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$response->dynamic(new Document([
'projects' => $output,
'total' => count($output),
]), Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST);
});
App::get('/v1/migrations/firebase/deauthorize')
->desc('Revoke Appwrite\'s authorization to access Firebase projects')
->groups(['api', 'migrations'])
->label('scope', 'migrations.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'migrations')
->label('sdk.method', 'deleteFirebaseAuth')
->label('sdk.description', '')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->inject('user')
->inject('response')
->inject('dbForConsole')
->action(function (Document $user, Response $response, Database $dbForConsole) {
$identity = $dbForConsole->findOne('identities', [
Query::equal('provider', ['firebase']),
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity->isEmpty()) {
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Not authenticated with Firebase'); //TODO: Replace with USER_IDENTITY_NOT_FOUND
}
$dbForConsole->deleteDocument('identities', $identity->getId());
$response->noContent();
});
App::get('/v1/migrations/supabase/report')
->groups(['api', 'migrations'])
->desc('Generate a report on Supabase Data')
+4 -4
View File
@@ -325,8 +325,8 @@ App::post('/v1/project/variables')
->inject('project')
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (string $key, string $value, Document $project, Response $response, Database $dbForProject, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $key, string $value, Document $project, Response $response, Database $dbForProject, Database $dbForPlatform) {
$variableId = ID::unique();
$variable = new Document([
@@ -429,8 +429,8 @@ App::put('/v1/project/variables/:variableId')
->inject('project')
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (string $variableId, string $key, ?string $value, Document $project, Response $response, Database $dbForProject, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $variableId, string $key, ?string $value, Document $project, Response $response, Database $dbForProject, Database $dbForPlatform) {
$variable = $dbForProject->getDocument('variables', $variableId);
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
File diff suppressed because it is too large Load Diff
+35 -25
View File
@@ -11,6 +11,7 @@ use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
@@ -42,9 +43,9 @@ App::post('/v1/proxy/rules')
->inject('project')
->inject('queueForCertificates')
->inject('queueForEvents')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('dbForProject')
->action(function (string $domain, string $resourceType, string $resourceId, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForConsole, Database $dbForProject) {
->action(function (string $domain, string $resourceType, string $resourceId, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject) {
$mainDomain = System::getEnv('_APP_DOMAIN', '');
if ($domain === $mainDomain) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'You cannot assign your main domain to specific resource. Please use subdomain or a different domain.');
@@ -59,8 +60,15 @@ App::post('/v1/proxy/rules')
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.');
}
$ruleId = md5($domain);
$document = $dbForConsole->getDocument('rules', $ruleId);
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$document = $dbForPlatform->getDocument('rules', md5($domain));
} else {
$document = $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain]),
]);
}
if (!$document->isEmpty()) {
if ($document->getAttribute('projectId') === $project->getId()) {
@@ -101,7 +109,9 @@ App::post('/v1/proxy/rules')
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
}
$ruleId = md5($domain->get());
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
$rule = new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
@@ -135,7 +145,7 @@ App::post('/v1/proxy/rules')
}
$rule->setAttribute('status', $status);
$rule = $dbForConsole->createDocument('rules', $rule);
$rule = $dbForPlatform->createDocument('rules', $rule);
$queueForEvents->setParam('ruleId', $rule->getId());
@@ -161,8 +171,8 @@ App::get('/v1/proxy/rules')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (array $queries, string $search, Response $response, Document $project, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (array $queries, string $search, Response $response, Document $project, Database $dbForPlatform) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@@ -191,7 +201,7 @@ App::get('/v1/proxy/rules')
}
$ruleId = $cursor->getValue();
$cursorDocument = $dbForConsole->getDocument('rules', $ruleId);
$cursorDocument = $dbForPlatform->getDocument('rules', $ruleId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Rule '{$ruleId}' for the 'cursor' value not found.");
@@ -202,16 +212,16 @@ App::get('/v1/proxy/rules')
$filterQueries = Query::groupByType($queries)['filters'];
$rules = $dbForConsole->find('rules', $queries);
$rules = $dbForPlatform->find('rules', $queries);
foreach ($rules as $rule) {
$certificate = $dbForConsole->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$rule->setAttribute('logs', $certificate->getAttribute('logs', ''));
$rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', ''));
}
$response->dynamic(new Document([
'rules' => $rules,
'total' => $dbForConsole->count('rules', $filterQueries, APP_LIMIT_COUNT),
'total' => $dbForPlatform->count('rules', $filterQueries, APP_LIMIT_COUNT),
]), Response::MODEL_PROXY_RULE_LIST);
});
@@ -229,15 +239,15 @@ App::get('/v1/proxy/rules/:ruleId')
->param('ruleId', '', new UID(), 'Rule ID.')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $ruleId, Response $response, Document $project, Database $dbForConsole) {
$rule = $dbForConsole->getDocument('rules', $ruleId);
->inject('dbForPlatform')
->action(function (string $ruleId, Response $response, Document $project, Database $dbForPlatform) {
$rule = $dbForPlatform->getDocument('rules', $ruleId);
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getInternalId()) {
throw new Exception(Exception::RULE_NOT_FOUND);
}
$certificate = $dbForConsole->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$rule->setAttribute('logs', $certificate->getAttribute('logs', ''));
$rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', ''));
@@ -260,17 +270,17 @@ App::delete('/v1/proxy/rules/:ruleId')
->param('ruleId', '', new UID(), 'Rule ID.')
->inject('response')
->inject('project')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForDeletes')
->inject('queueForEvents')
->action(function (string $ruleId, Response $response, Document $project, Database $dbForConsole, Delete $queueForDeletes, Event $queueForEvents) {
$rule = $dbForConsole->getDocument('rules', $ruleId);
->action(function (string $ruleId, Response $response, Document $project, Database $dbForPlatform, Delete $queueForDeletes, Event $queueForEvents) {
$rule = $dbForPlatform->getDocument('rules', $ruleId);
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getInternalId()) {
throw new Exception(Exception::RULE_NOT_FOUND);
}
$dbForConsole->deleteDocument('rules', $rule->getId());
$dbForPlatform->deleteDocument('rules', $rule->getId());
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
@@ -299,10 +309,10 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
->inject('queueForCertificates')
->inject('queueForEvents')
->inject('project')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('log')
->action(function (string $ruleId, Response $response, Certificate $queueForCertificates, Event $queueForEvents, Document $project, Database $dbForConsole, Log $log) {
$rule = $dbForConsole->getDocument('rules', $ruleId);
->action(function (string $ruleId, Response $response, Certificate $queueForCertificates, Event $queueForEvents, Document $project, Database $dbForPlatform, Log $log) {
$rule = $dbForPlatform->getDocument('rules', $ruleId);
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getInternalId()) {
throw new Exception(Exception::RULE_NOT_FOUND);
@@ -332,7 +342,7 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
throw new Exception(Exception::RULE_VERIFICATION_FAILED);
}
$dbForConsole->updateDocument('rules', $rule->getId(), $rule->setAttribute('status', 'verifying'));
$dbForPlatform->updateDocument('rules', $rule->getId(), $rule->setAttribute('status', 'verifying'));
// Issue a TLS certificate when domain is verified
$queueForCertificates
@@ -343,7 +353,7 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
$queueForEvents->setParam('ruleId', $rule->getId());
$certificate = $dbForConsole->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$rule->setAttribute('logs', $certificate->getAttribute('logs', ''));
$response->dynamic($rule, Response::MODEL_PROXY_RULE);
+11 -3
View File
@@ -6,6 +6,7 @@ use Appwrite\Auth\Auth;
use Appwrite\ClamAV\Network;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Usage;
use Appwrite\Extend\Exception;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Utopia\Database\Validator\CustomId;
@@ -74,13 +75,14 @@ App::post('/v1/storage/buckets')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, ?string $compression, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $queueForEvents) {
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $queueForEvents) {
$bucketId = $bucketId === 'unique()' ? ID::unique() : $bucketId;
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions);
$compression ??= Compression::NONE;
$encryption ??= true;
try {
$files = (Config::getParam('collections', [])['buckets'] ?? [])['files'] ?? [];
if (empty($files)) {
@@ -260,7 +262,7 @@ App::put('/v1/storage/buckets/:bucketId')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, ?string $compression, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $queueForEvents) {
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $queueForEvents) {
$bucket = $dbForProject->getDocument('buckets', $bucketId);
if ($bucket->isEmpty()) {
@@ -885,7 +887,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
->inject('mode')
->inject('deviceForFiles')
->inject('deviceForLocal')
->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, Request $request, Response $response, Document $project, Database $dbForProject, string $mode, Device $deviceForFiles, Device $deviceForLocal) {
->inject('queueForUsage')
->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, Request $request, Response $response, Document $project, Database $dbForProject, string $mode, Device $deviceForFiles, Device $deviceForLocal, Usage $queueForUsage) {
if (!\extension_loaded('imagick')) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
@@ -1013,6 +1016,11 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'];
$queueForUsage
->addMetric(METRIC_FILES_TRANSFORMATIONS, 1)
->addMetric(str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_TRANSFORMATIONS), 1)
;
$response
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
->setContentType($contentType)
+77 -57
View File
@@ -804,8 +804,11 @@ App::get('/v1/teams/:teamId/memberships')
}, $membershipsPrivacy);
$memberships = array_map(function ($membership) use ($dbForProject, $team, $membershipsPrivacy) {
$user = !empty(array_filter($membershipsPrivacy))
? $dbForProject->getDocument('users', $membership->getAttribute('userId'))
: new Document();
if ($membershipsPrivacy['mfa']) {
$user = $dbForProject->getDocument('users', $membership->getAttribute('userId'));
$mfa = $user->getAttribute('mfa', false);
if ($mfa) {
@@ -887,9 +890,11 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
return $privacy || $isPrivilegedUser || $isAppUser;
}, $membershipsPrivacy);
if ($membershipsPrivacy['mfa']) {
$user = $dbForProject->getDocument('users', $membership->getAttribute('userId'));
$user = !empty(array_filter($membershipsPrivacy))
? $dbForProject->getDocument('users', $membership->getAttribute('userId'))
: new Document();
if ($membershipsPrivacy['mfa']) {
$mfa = $user->getAttribute('mfa', false);
if ($mfa) {
@@ -1054,7 +1059,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
throw new Exception(Exception::TEAM_INVITE_MISMATCH, 'Invite does not belong to current user (' . $user->getAttribute('email') . ')');
}
if ($user->isEmpty()) {
$hasSession = !$user->isEmpty();
if (!$hasSession) {
$user->setAttributes($dbForProject->getDocument('users', $userId)->getArrayCopy()); // Get user
}
@@ -1073,50 +1079,75 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
Authorization::skip(fn () => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true)));
// Log user in
// Create session for the user if not logged in
if (!$hasSession) {
Authorization::setRole(Role::user($user->getId())->toString());
Authorization::setRole(Role::user($user->getId())->toString());
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::addSeconds(new \DateTime(), $authDuration);
$secret = Auth::tokenGenerator();
$session = new Document(array_merge([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
],
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => $user->getAttribute('email'),
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'factors' => ['email'],
'countryCode' => \strtolower($geoRecord['countryCode']),
'continentCode' => strtolower($geoRecord['continentCode']),
'latitude' => $geoRecord['latitude'],
'longitude' => $geoRecord['longitude'],
'timeZone' => $geoRecord['timeZone'],
'weatherCode' => $geoRecord['weatherCode'],
'postalCode' => $geoRecord['postalCode'],
'isp' => $geoRecord['isp'],
'autonomousSystemNumber' => $geoRecord['autonomousSystemNumber'],
'autonomousSystemOrganization' => $geoRecord['autonomousSystemOrganization'],
'connectionType' => $geoRecord['connectionType'],
'userType' => $geoRecord['userType'],
'organization' => $geoRecord['organization'],
'expire' => DateTime::addSeconds(new \DateTime(), $authDuration)
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::addSeconds(new \DateTime(), $authDuration);
$secret = Auth::tokenGenerator();
$session = new Document(array_merge([
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => $user->getAttribute('email'),
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'factors' => ['email'],
'countryCode' => \strtolower($geoRecord['countryCode']),
'continentCode' => strtolower($geoRecord['continentCode']),
'latitude' => $geoRecord['latitude'],
'longitude' => $geoRecord['longitude'],
'timeZone' => $geoRecord['timeZone'],
'weatherCode' => $geoRecord['weatherCode'],
'postalCode' => $geoRecord['postalCode'],
'isp' => $geoRecord['isp'],
'autonomousSystemNumber' => $geoRecord['autonomousSystemNumber'],
'autonomousSystemOrganization' => $geoRecord['autonomousSystemOrganization'],
'connectionType' => $geoRecord['connectionType'],
'userType' => $geoRecord['userType'],
'organization' => $geoRecord['organization'],
'expire' => DateTime::addSeconds(new \DateTime(), $authDuration)
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
$session = $dbForProject->createDocument('sessions', $session);
$session = $dbForProject->createDocument('sessions', $session
->setAttribute('$permissions', [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
]));
Authorization::setRole(Role::user($userId)->toString());
$dbForProject->purgeCachedDocument('users', $user->getId());
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]));
}
Authorization::setRole(Role::user($userId)->toString());
$response
->addCookie(
name: Auth::$cookieName . '_legacy',
value: Auth::encodeSession($user->getId(), $secret),
expire: (new \DateTime($expire))->getTimestamp(),
path: '/',
domain: Config::getParam('cookieDomain'),
secure: ('https' === $protocol),
httponly: true
)
->addCookie(
name: Auth::$cookieName,
value: Auth::encodeSession($user->getId(), $secret),
expire: (new \DateTime($expire))->getTimestamp(),
path: '/',
domain: Config::getParam('cookieDomain'),
secure: ('https' === $protocol),
httponly: true,
sameSite: Config::getParam('cookieSamesite')
)
;
}
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership);
@@ -1130,22 +1161,11 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->setParam('membershipId', $membership->getId())
;
if (!Config::getParam('domainVerification')) {
$response
->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]))
;
}
$response
->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
;
$response->dynamic(
$membership
->setAttribute('teamName', $team->getAttribute('name'))
->setAttribute('userName', $user->getAttribute('name'))
->setAttribute('userEmail', $user->getAttribute('email')),
->setAttribute('teamName', $team->getAttribute('name'))
->setAttribute('userName', $user->getAttribute('name'))
->setAttribute('userEmail', $user->getAttribute('email')),
Response::MODEL_MEMBERSHIP
);
});
+5 -1
View File
@@ -1795,6 +1795,7 @@ App::post('/v1/users/:userId/sessions')
'provider' => Auth::SESSION_PROVIDER_SERVER,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'factors' => ['server'],
'ip' => $request->getIP(),
'countryCode' => \strtolower($geoRecord['countryCode']),
'continentCode' => strtolower($geoRecord['continentCode']),
@@ -1819,8 +1820,11 @@ App::post('/v1/users/:userId/sessions')
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
$session = $dbForProject->createDocument('sessions', $session);
$dbForProject->purgeCachedDocument('users', $user->getId());
$session
->setAttribute('secret', $secret)
->setAttribute('secret', Auth::encodeSession($user->getId(), $secret))
->setAttribute('countryName', $countryName);
$queueForEvents
+64 -64
View File
@@ -42,7 +42,7 @@ use Utopia\VCS\Exception\RepositoryNotFound;
use function Swoole\Coroutine\batch;
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForConsole, Build $queueForBuilds, callable $getProjectDB, Request $request) {
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForPlatform, Build $queueForBuilds, callable $getProjectDB, Request $request) {
$errors = [];
foreach ($repositories as $resource) {
try {
@@ -53,7 +53,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
}
$projectId = $resource->getAttribute('projectId');
$project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId));
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
$dbForProject = $getProjectDB($project);
$functionId = $resource->getAttribute('resourceId');
@@ -104,7 +104,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$latestCommentId = '';
if (!empty($providerPullRequestId) && $function->getAttribute('providerSilentMode', false) === false) {
$latestComment = Authorization::skip(fn () => $dbForConsole->findOne('vcsComments', [
$latestComment = Authorization::skip(fn () => $dbForPlatform->findOne('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerPullRequestId', [$providerPullRequestId]),
Query::orderDesc('$createdAt'),
@@ -125,7 +125,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
if (!empty($latestCommentId)) {
$teamId = $project->getAttribute('teamId', '');
$latestComment = Authorization::skip(fn () => $dbForConsole->createDocument('vcsComments', new Document([
$latestComment = Authorization::skip(fn () => $dbForPlatform->createDocument('vcsComments', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
@@ -146,7 +146,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
}
}
} elseif (!empty($providerBranch)) {
$latestComments = Authorization::skip(fn () => $dbForConsole->find('vcsComments', [
$latestComments = Authorization::skip(fn () => $dbForPlatform->find('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerBranch', [$providerBranch]),
Query::orderDesc('$createdAt'),
@@ -319,8 +319,8 @@ App::get('/v1/vcs/github/callback')
->inject('project')
->inject('request')
->inject('response')
->inject('dbForConsole')
->action(function (string $providerInstallationId, string $setupAction, string $state, string $code, GitHub $github, Document $user, Document $project, Request $request, Response $response, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $providerInstallationId, string $setupAction, string $state, string $code, GitHub $github, Document $user, Document $project, Request $request, Response $response, Database $dbForPlatform) {
if (empty($state)) {
$error = 'Installation requests from organisation members for the Appwrite GitHub App are currently unsupported. To proceed with the installation, login to the Appwrite Console and install the GitHub App.';
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $error);
@@ -339,7 +339,7 @@ App::get('/v1/vcs/github/callback')
$redirectSuccess = $state['success'] ?? '';
$redirectFailure = $state['failure'] ?? '';
$project = $dbForConsole->getDocument('projects', $projectId);
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
$error = 'Project with the ID from state could not be found.';
@@ -368,7 +368,7 @@ App::get('/v1/vcs/github/callback')
$oauth2ID = $oauth2->getUserID($accessToken);
// Makes sure this email is not already used in another identity
$identity = $dbForConsole->findOne('identities', [
$identity = $dbForPlatform->findOne('identities', [
Query::equal('providerEmail', [$email]),
]);
if (!$identity->isEmpty()) {
@@ -381,9 +381,9 @@ App::get('/v1/vcs/github/callback')
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
$dbForPlatform->updateDocument('identities', $identity->getId(), $identity);
} else {
$identity = $dbForConsole->createDocument('identities', new Document([
$identity = $dbForPlatform->createDocument('identities', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
@@ -411,7 +411,7 @@ App::get('/v1/vcs/github/callback')
$projectInternalId = $project->getInternalId();
$installation = $dbForConsole->findOne('installations', [
$installation = $dbForPlatform->findOne('installations', [
Query::equal('providerInstallationId', [$providerInstallationId]),
Query::equal('projectInternalId', [$projectInternalId])
]);
@@ -436,12 +436,12 @@ App::get('/v1/vcs/github/callback')
'personal' => $personalSlug === $owner
]);
$installation = $dbForConsole->createDocument('installations', $installation);
$installation = $dbForPlatform->createDocument('installations', $installation);
} else {
$installation = $installation
->setAttribute('organization', $owner)
->setAttribute('personal', $personalSlug === $owner);
$installation = $dbForConsole->updateDocument('installations', $installation->getId(), $installation);
$installation = $dbForPlatform->updateDocument('installations', $installation->getId(), $installation);
}
} else {
$error = 'Installation of the Appwrite GitHub App on organization accounts is restricted to organization owners. As a member of the organization, you do not have the necessary permissions to install this GitHub App. Please contact the organization owner to create the installation from the Appwrite console.';
@@ -480,9 +480,9 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, string $providerRepositoryId, string $providerRootDirectory, GitHub $github, Response $response, Document $project, Database $dbForConsole) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->inject('dbForPlatform')
->action(function (string $installationId, string $providerRepositoryId, string $providerRootDirectory, GitHub $github, Response $response, Document $project, Database $dbForPlatform) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -541,9 +541,9 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:pr
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, string $providerRepositoryId, string $providerRootDirectory, GitHub $github, Response $response, Document $project, Database $dbForConsole) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->inject('dbForPlatform')
->action(function (string $installationId, string $providerRepositoryId, string $providerRootDirectory, GitHub $github, Response $response, Document $project, Database $dbForPlatform) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -612,13 +612,13 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, string $search, GitHub $github, Response $response, Document $project, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $installationId, string $search, GitHub $github, Response $response, Document $project, Database $dbForPlatform) {
if (empty($search)) {
$search = "";
}
$installation = $dbForConsole->getDocument('installations', $installationId);
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -709,9 +709,9 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories')
->inject('user')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, string $name, bool $private, GitHub $github, Document $user, Response $response, Document $project, Database $dbForConsole) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->inject('dbForPlatform')
->action(function (string $installationId, string $name, bool $private, GitHub $github, Document $user, Response $response, Document $project, Database $dbForPlatform) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -720,7 +720,7 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories')
if ($installation->getAttribute('personal', false) === true) {
$oauth2 = new OAuth2Github(System::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''), System::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', ''), "");
$identity = $dbForConsole->findOne('identities', [
$identity = $dbForPlatform->findOne('identities', [
Query::equal('provider', ['github']),
Query::equal('userInternalId', [$user->getInternalId()]),
]);
@@ -750,7 +750,7 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories')
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry('')));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
$dbForPlatform->updateDocument('identities', $identity->getId(), $identity);
}
try {
@@ -808,9 +808,9 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, string $providerRepositoryId, GitHub $github, Response $response, Document $project, Database $dbForConsole) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->inject('dbForPlatform')
->action(function (string $installationId, string $providerRepositoryId, GitHub $github, Response $response, Document $project, Database $dbForPlatform) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -857,9 +857,9 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, string $providerRepositoryId, GitHub $github, Response $response, Document $project, Database $dbForConsole) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->inject('dbForPlatform')
->action(function (string $installationId, string $providerRepositoryId, GitHub $github, Response $response, Document $project, Database $dbForPlatform) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -897,11 +897,11 @@ App::post('/v1/vcs/github/events')
->inject('gitHub')
->inject('request')
->inject('response')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForBuilds')
->action(
function (GitHub $github, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
$payload = $request->getRawPayload();
$signatureRemote = $request->getHeader('x-hub-signature-256', '');
$signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', '');
@@ -935,36 +935,36 @@ App::post('/v1/vcs/github/events')
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
//find functionId from functions table
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::limit(100),
]));
// create new deployment only on push and not when branch is created
if (!$providerBranchCreated) {
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForConsole, $queueForBuilds, $getProjectDB, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
}
} elseif ($event == $github::EVENT_INSTALLATION) {
if ($parsedPayload["action"] == "deleted") {
// TODO: Use worker for this job instead (update function as well)
$providerInstallationId = $parsedPayload["installationId"];
$installations = $dbForConsole->find('installations', [
$installations = $dbForPlatform->find('installations', [
Query::equal('providerInstallationId', [$providerInstallationId]),
Query::limit(1000)
]);
foreach ($installations as $installation) {
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('installationInternalId', [$installation->getInternalId()]),
Query::limit(1000)
]));
foreach ($repositories as $repository) {
Authorization::skip(fn () => $dbForConsole->deleteDocument('repositories', $repository->getId()));
Authorization::skip(fn () => $dbForPlatform->deleteDocument('repositories', $repository->getId()));
}
$dbForConsole->deleteDocument('installations', $installation->getId());
$dbForPlatform->deleteDocument('installations', $installation->getId());
}
}
} elseif ($event == $github::EVENT_PULL_REQUEST) {
@@ -993,12 +993,12 @@ App::post('/v1/vcs/github/events')
$providerCommitAuthor = $commitDetails["commitAuthor"] ?? '';
$providerCommitMessage = $commitDetails["commitMessage"] ?? '';
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt')
]));
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForConsole, $queueForBuilds, $getProjectDB, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
} elseif ($parsedPayload["action"] == "closed") {
// Allowed external contributions cleanup
@@ -1007,7 +1007,7 @@ App::post('/v1/vcs/github/events')
$external = $parsedPayload["external"] ?? true;
if ($external) {
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt')
]));
@@ -1018,7 +1018,7 @@ App::post('/v1/vcs/github/events')
if (\in_array($providerPullRequestId, $providerPullRequestIds)) {
$providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]);
$repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds);
$repository = Authorization::skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository));
$repository = Authorization::skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
}
}
}
@@ -1045,8 +1045,8 @@ App::get('/v1/vcs/installations')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (array $queries, string $search, Response $response, Document $project, Database $dbForProject, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (array $queries, string $search, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@@ -1075,7 +1075,7 @@ App::get('/v1/vcs/installations')
}
$installationId = $cursor->getValue();
$cursorDocument = $dbForConsole->getDocument('installations', $installationId);
$cursorDocument = $dbForPlatform->getDocument('installations', $installationId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Installation '{$installationId}' for the 'cursor' value not found.");
@@ -1086,8 +1086,8 @@ App::get('/v1/vcs/installations')
$filterQueries = Query::groupByType($queries)['filters'];
$results = $dbForConsole->find('installations', $queries);
$total = $dbForConsole->count('installations', $filterQueries, APP_LIMIT_COUNT);
$results = $dbForPlatform->find('installations', $queries);
$total = $dbForPlatform->count('installations', $filterQueries, APP_LIMIT_COUNT);
$response->dynamic(new Document([
'installations' => $results,
@@ -1109,9 +1109,9 @@ App::get('/v1/vcs/installations/:installationId')
->param('installationId', '', new Text(256), 'Installation Id')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, Response $response, Document $project, Database $dbForConsole) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->inject('dbForPlatform')
->action(function (string $installationId, Response $response, Document $project, Database $dbForPlatform) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation === false || $installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -1137,16 +1137,16 @@ App::delete('/v1/vcs/installations/:installationId')
->param('installationId', '', new Text(256), 'Installation Id')
->inject('response')
->inject('project')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForDeletes')
->action(function (string $installationId, Response $response, Document $project, Database $dbForConsole, Delete $queueForDeletes) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->action(function (string $installationId, Response $response, Document $project, Database $dbForPlatform, Delete $queueForDeletes) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
if (!$dbForConsole->deleteDocument('installations', $installation->getId())) {
if (!$dbForPlatform->deleteDocument('installations', $installation->getId())) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove installation from DB');
}
@@ -1174,17 +1174,17 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
->inject('request')
->inject('response')
->inject('project')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForBuilds')
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$repository = Authorization::skip(fn () => $dbForConsole->getDocument('repositories', $repositoryId, [
$repository = Authorization::skip(fn () => $dbForPlatform->getDocument('repositories', $repositoryId, [
Query::equal('projectInternalId', [$project->getInternalId()])
]));
@@ -1201,7 +1201,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
// TODO: Delete from array when PR is closed
$repository = Authorization::skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository));
$repository = Authorization::skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
@@ -1225,7 +1225,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
$providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? '';
$providerCommitHash = $pullRequestResponse['head']['sha'] ?? '';
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForConsole, $queueForBuilds, $getProjectDB, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
$response->noContent();
});
+94 -51
View File
@@ -28,6 +28,7 @@ use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Domains\Domain;
use Utopia\DSN\DSN;
@@ -44,13 +45,26 @@ Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(App $utopia, Database $dbForConsole, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, array $geoRecord, callable $isResourceBlocked)
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, array $geoRecord, callable $isResourceBlocked, string $previewHostname)
{
$utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml');
$host = $request->getHostname() ?? '';
if (!empty($previewHostname)) {
$host = $previewHostname;
}
$route = Authorization::skip(fn () => $dbForConsole->getDocument('rules', md5($host)));
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$route = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($host)));
} else {
$route = Authorization::skip(
fn () => $dbForPlatform->find('rules', [
Query::equal('domain', [$host]),
Query::limit(1)
])
)[0] ?? new Document();
}
if ($route->isEmpty()) {
if ($host === System::getEnv('_APP_DOMAIN_FUNCTIONS', '')) {
@@ -62,7 +76,7 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
}
if (System::getEnv('_APP_OPTIONS_ROUTER_PROTECTION', 'disabled') === 'enabled') {
if ($host !== 'localhost' && $host !== APP_HOSTNAME_INTERNAL) { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations
if ($host !== 'localhost' && $host !== APP_HOSTNAME_INTERNAL && $host !== System::getEnv('_APP_CONSOLE_DOMAIN', '')) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Router protection does not allow accessing Appwrite over this domain. Please add it as custom domain to your project or disable _APP_OPTIONS_ROUTER_PROTECTION environment variable.');
}
}
@@ -74,8 +88,17 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
$projectId = $route->getAttribute('projectId');
$project = Authorization::skip(
fn () => $dbForConsole->getDocument('projects', $projectId)
fn () => $dbForPlatform->getDocument('projects', $projectId)
);
if (!$project->isEmpty() && $project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$project->setAttribute('accessedAt', DateTime::now());
Authorization::skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
}
}
if (array_key_exists('proxy', $project->getAttribute('services', []))) {
$status = $project->getAttribute('services', [])['proxy'];
if (!$status) {
@@ -120,7 +143,7 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
$requestHeaders = $request->getHeaders();
$project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId));
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
$dbForProject = $getProjectDB($project);
@@ -334,26 +357,6 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
throw $th;
}
} finally {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$queueForUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize())
->addMetric(METRIC_EXECUTIONS, 1)
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1)
->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->setProject($project)
->trigger()
;
$queueForFunctions
->setType(Func::TYPE_ASYNC_WRITE)
->setExecution($execution)
@@ -388,6 +391,26 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
->setStatusCode($execution['responseStatusCode'] ?? 200)
->send($body);
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$queueForUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize())
->addMetric(METRIC_EXECUTIONS, 1)
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1)
->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->setProject($project)
->trigger()
;
return true;
} elseif ($type === 'api') {
$utopia->getRoute()?->label('error', '');
@@ -433,7 +456,7 @@ App::init()
->inject('response')
->inject('console')
->inject('project')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('locale')
->inject('localeCodes')
@@ -444,15 +467,16 @@ App::init()
->inject('queueForCertificates')
->inject('queueForFunctions')
->inject('isResourceBlocked')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, callable $getProjectDB, Locale $locale, array $localeCodes, array $clients, array $geoRecord, Usage $queueForUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, callable $isResourceBlocked) {
->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, array $clients, array $geoRecord, Usage $queueForUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, callable $isResourceBlocked, string $previewHostname) {
/*
* Appwrite Router
*/
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain
if ($host !== $mainDomain) {
if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geoRecord, $isResourceBlocked)) {
if ($host !== $mainDomain || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geoRecord, $isResourceBlocked, $previewHostname)) {
return;
}
}
@@ -500,18 +524,31 @@ App::init()
if (!empty($envDomain) && $envDomain !== 'localhost') {
$mainDomain = $envDomain;
} else {
$domainDocument = $dbForConsole->getDocument('rules', md5($envDomain));
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$domainDocument = $dbForPlatform->getDocument('rules', md5($envDomain));
} else {
$domainDocument = $dbForPlatform->findOne('rules', [Query::orderAsc('$id')]);
}
$mainDomain = !$domainDocument->isEmpty() ? $domainDocument->getAttribute('domain') : $domain->get();
}
if ($mainDomain !== $domain->get()) {
Console::warning($domain->get() . ' is not a main domain. Skipping SSL certificate generation.');
} else {
$domainDocument = $dbForConsole->getDocument('rules', md5($domain->get()));
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$domainDocument = $dbForPlatform->getDocument('rules', md5($domain->get()));
} else {
$domainDocument = $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain->get()])
]);
}
if ($domainDocument->isEmpty()) {
$domainDocument = new Document([
'$id' => md5($domain->get()),
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
'$id' => System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(),
'domain' => $domain->get(),
'resourceType' => 'api',
'status' => 'verifying',
@@ -519,7 +556,7 @@ App::init()
'projectInternalId' => 'console'
]);
$domainDocument = $dbForConsole->createDocument('rules', $domainDocument);
$domainDocument = $dbForPlatform->createDocument('rules', $domainDocument);
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
@@ -655,22 +692,23 @@ App::options()
->inject('swooleRequest')
->inject('request')
->inject('response')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForUsage')
->inject('queueForFunctions')
->inject('geoRecord')
->inject('isResourceBlocked')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, array $geoRecord, callable $isResourceBlocked) {
->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, array $geoRecord, callable $isResourceBlocked, string $previewHostname) {
/*
* Appwrite Router
*/
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain
if ($host !== $mainDomain) {
if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geoRecord, $isResourceBlocked)) {
if ($host !== $mainDomain || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geoRecord, $isResourceBlocked, $previewHostname)) {
return;
}
}
@@ -785,7 +823,7 @@ App::error()
$adapter = new Sentry($projectId, $key, $host);
$logger = new Logger($adapter);
$logger->setSample(0.04);
$logger->setSample(0.01);
$publish = true;
} else {
throw new \Exception('Invalid experimental logging provider');
@@ -825,6 +863,8 @@ App::error()
if (isset($user) && !$user->isEmpty()) {
$log->setUser(new User($user->getId()));
} else {
$log->setUser(new User('guest-' . hash('sha256', $request->getIP())));
}
try {
@@ -835,7 +875,7 @@ App::error()
}
$log->setNamespace("http");
$log->setServer(\gethostname());
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
@@ -856,6 +896,7 @@ App::error()
$action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD");
$log->setAction($action);
$log->addTag('service', $action);
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
@@ -950,22 +991,23 @@ App::get('/robots.txt')
->inject('swooleRequest')
->inject('request')
->inject('response')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForUsage')
->inject('queueForFunctions')
->inject('geoRecord')
->inject('isResourceBlocked')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, array $geoRecord, callable $isResourceBlocked) {
->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, array $geoRecord, callable $isResourceBlocked, string $previewHostname) {
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
if ($host === $mainDomain || $host === 'localhost') {
if (($host === $mainDomain || $host === 'localhost') && empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geoRecord, $isResourceBlocked);
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geoRecord, $isResourceBlocked, $previewHostname);
}
});
@@ -977,22 +1019,23 @@ App::get('/humans.txt')
->inject('swooleRequest')
->inject('request')
->inject('response')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForUsage')
->inject('queueForFunctions')
->inject('geoRecord')
->inject('isResourceBlocked')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, array $geoRecord, callable $isResourceBlocked) {
->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, array $geoRecord, callable $isResourceBlocked, string $previewHostname) {
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
if ($host === $mainDomain || $host === 'localhost') {
if (($host === $mainDomain || $host === 'localhost') && empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geoRecord, $isResourceBlocked);
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geoRecord, $isResourceBlocked, $previewHostname);
}
});
@@ -1056,9 +1099,9 @@ App::get('/v1/ping')
->label('event', 'projects.[projectId].ping')
->inject('response')
->inject('project')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForEvents')
->action(function (Response $response, Document $project, Database $dbForConsole, Event $queueForEvents) {
->action(function (Response $response, Document $project, Database $dbForPlatform, Event $queueForEvents) {
if ($project->isEmpty()) {
throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND);
}
@@ -1070,8 +1113,8 @@ App::get('/v1/ping')
->setAttribute('pingCount', $pingCount)
->setAttribute('pingedAt', $pingedAt);
Authorization::skip(function () use ($dbForConsole, $project) {
$dbForConsole->updateDocument('projects', $project->getId(), $project);
Authorization::skip(function () use ($dbForPlatform, $project) {
$dbForPlatform->updateDocument('projects', $project->getId(), $project);
});
$queueForEvents
+9 -9
View File
@@ -162,15 +162,15 @@ App::post('/v1/mock/api-key-unprefixed')
->label('docs', false)
->param('projectId', '', new UID(), 'Project ID.')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, Response $response, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $projectId, Response $response, Database $dbForPlatform) {
$isDevelopment = System::getEnv('_APP_ENV', 'development') === 'development';
if (!$isDevelopment) {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
}
$project = $dbForConsole->getDocument('projects', $projectId);
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
@@ -195,9 +195,9 @@ App::post('/v1/mock/api-key-unprefixed')
'secret' => \bin2hex(\random_bytes(128)),
]);
$key = $dbForConsole->createDocument('keys', $key);
$key = $dbForPlatform->createDocument('keys', $key);
$dbForConsole->purgeCachedDocument('projects', $project->getId());
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -214,15 +214,15 @@ App::get('/v1/mock/github/callback')
->inject('gitHub')
->inject('project')
->inject('response')
->inject('dbForConsole')
->action(function (string $providerInstallationId, string $projectId, GitHub $github, Document $project, Response $response, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $providerInstallationId, string $projectId, GitHub $github, Document $project, Response $response, Database $dbForPlatform) {
$isDevelopment = System::getEnv('_APP_ENV', 'development') === 'development';
if (!$isDevelopment) {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
}
$project = $dbForConsole->getDocument('projects', $projectId);
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
$error = 'Project with the ID from state could not be found.';
@@ -256,7 +256,7 @@ App::get('/v1/mock/github/callback')
'personal' => false
]);
$installation = $dbForConsole->createDocument('installations', $installation);
$installation = $dbForPlatform->createDocument('installations', $installation);
}
$response->json([
+39 -42
View File
@@ -19,7 +19,6 @@ use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\Database\TimeLimit;
use Utopia\App;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
@@ -184,14 +183,15 @@ App::init()
->groups(['api'])
->inject('utopia')
->inject('request')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('dbForProject')
->inject('project')
->inject('user')
->inject('session')
->inject('servers')
->inject('mode')
->inject('team')
->action(function (App $utopia, Request $request, Database $dbForConsole, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team) {
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team) {
$route = $utopia->getRoute();
if ($project->isEmpty()) {
@@ -283,8 +283,8 @@ App::init()
$accessedAt = $key->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
$key->setAttribute('accessedAt', DateTime::now());
$dbForConsole->updateDocument('keys', $key->getId(), $key);
$dbForConsole->purgeCachedDocument('projects', $project->getId());
$dbForPlatform->updateDocument('keys', $key->getId(), $key);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
$sdkValidator = new WhiteList($servers, true);
@@ -297,8 +297,8 @@ App::init()
/** Update access time as well */
$key->setAttribute('accessedAt', Datetime::now());
$dbForConsole->updateDocument('keys', $key->getId(), $key);
$dbForConsole->purgeCachedDocument('projects', $project->getId());
$dbForPlatform->updateDocument('keys', $key->getId(), $key);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
}
}
@@ -335,6 +335,33 @@ App::init()
Authorization::setRole($authRole);
}
/**
* Update project last activity
*/
if (!$project->isEmpty() && $project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$project->setAttribute('accessedAt', DateTime::now());
Authorization::skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
}
}
/**
* Update user last activity
*/
if (!empty($user->getId())) {
$accessedAt = $user->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
$user->setAttribute('accessedAt', DateTime::now());
if (APP_MODE_ADMIN !== $mode) {
$dbForProject->updateDocument('users', $user->getId(), $user);
} else {
$dbForPlatform->updateDocument('users', $user->getId(), $user);
}
}
}
/** Do not allow access to disabled services */
$service = $route->getLabel('sdk.namespace', '');
if (!empty($service)) {
@@ -392,8 +419,9 @@ App::init()
->inject('queueForBuilds')
->inject('queueForUsage')
->inject('dbForProject')
->inject('timelimit')
->inject('mode')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Connection $queue, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Usage $queueForUsage, Database $dbForProject, string $mode) use ($usageDatabaseListener, $eventDatabaseListener) {
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Connection $queue, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Usage $queueForUsage, Database $dbForProject, callable $timelimit, string $mode) use ($usageDatabaseListener, $eventDatabaseListener) {
$route = $utopia->getRoute();
@@ -416,7 +444,7 @@ App::init()
foreach ($abuseKeyLabel as $abuseKey) {
$start = $request->getContentRangeStart();
$end = $request->getContentRangeEnd();
$timeLimit = new TimeLimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600), $dbForProject);
$timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600));
$timeLimit
->setParam('{projectId}', $project->getId())
->setParam('{userId}', $user->getId())
@@ -444,7 +472,7 @@ App::init()
$abuse = new Abuse($timeLimit);
$remaining = $timeLimit->remaining();
$limit = $timeLimit->limit();
$time = (new \DateTime($timeLimit->time()))->getTimestamp() + $route->getLabel('abuse-time', 3600);
$time = $timeLimit->time() + $route->getLabel('abuse-time', 3600);
if ($limit && ($remaining < $closestLimit || is_null($closestLimit))) {
$closestLimit = $remaining;
@@ -641,9 +669,7 @@ App::shutdown()
->inject('queueForWebhooks')
->inject('queueForRealtime')
->inject('dbForProject')
->inject('mode')
->inject('dbForConsole')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, Usage $queueForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, string $mode, Database $dbForConsole) use ($parseLabel) {
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, Usage $queueForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -763,8 +789,6 @@ App::shutdown()
}
}
if ($project->getId() !== 'console') {
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
$fileSize = 0;
@@ -783,33 +807,6 @@ App::shutdown()
->setProject($project)
->trigger();
}
/**
* Update project last activity
*/
if (!$project->isEmpty() && $project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$project->setAttribute('accessedAt', DateTime::now());
Authorization::skip(fn () => $dbForConsole->updateDocument('projects', $project->getId(), $project));
}
}
/**
* Update user last activity
*/
if (!$user->isEmpty()) {
$accessedAt = $user->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
$user->setAttribute('accessedAt', DateTime::now());
if (APP_MODE_ADMIN !== $mode) {
$dbForProject->updateDocument('users', $user->getId(), $user);
} else {
$dbForConsole->updateDocument('users', $user->getId(), $user);
}
}
}
});
App::init()
+222 -73
View File
@@ -9,16 +9,19 @@ use Swoole\Http\Request as SwooleRequest;
use Swoole\Http\Response as SwooleResponse;
use Swoole\Http\Server;
use Swoole\Process;
use Utopia\Abuse\Adapters\Database\TimeLimit;
use Swoole\Table;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Logger\Log\User;
@@ -26,6 +29,12 @@ use Utopia\Pools\Group;
use Utopia\Swoole\Files;
use Utopia\System\System;
const DOMAIN_SYNC_TIMER = 30; // 30 seconds
$domains = new Table(1_000_000); // 1 million rows
$domains->column('value', Table::TYPE_INT, 1);
$domains->create();
$http = new Server(
host: "0.0.0.0",
port: System::getEnv('PORT', 80),
@@ -33,15 +42,17 @@ $http = new Server(
);
$payloadSize = 12 * (1024 * 1024); // 12MB - adding slight buffer for headers and other data that might be sent with the payload - update later with valid testing
$workerNumber = swoole_cpu_num() * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$http
->set([
'worker_num' => $workerNumber,
'worker_num' => $totalWorkers,
'dispatch_func' => 'dispatch',
'open_http2_protocol' => true,
'http_compression' => false,
'package_max_length' => $payloadSize,
'buffer_output_size' => $payloadSize,
'task_worker_num' => 1, // required for the task to fetch domains background
]);
$http->on(Constant::EVENT_WORKER_START, function ($server, $workerId) {
@@ -56,6 +67,93 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server, $workerId) {
Console::success('Reload completed...');
});
/**
* Assigns HTTP requests to worker threads by analyzing its payload/content.
*
* Routes requests as 'safe' or 'risky' based on specific content patterns (like POST actions or certain domains)
* to optimize load distribution between the workers. Utilizes `$safeThreadsPercent` to manage risk by assigning
* riskier tasks to a dedicated worker subset. Prefers idle workers, with fallback to random selection if necessary.
* doc: https://openswoole.com/docs/modules/swoole-server/configuration#dispatch_func
*
* @param Server $server Swoole server instance.
* @param int $fd client ID
* @param int $type the type of data and its current state
* @param string|null $data Request content for categorization.
* @global int $totalThreads Total number of workers.
* @return int Chosen worker ID for the request.
*/
function dispatch(Server $server, int $fd, int $type, $data = null): int
{
global $totalWorkers, $domains;
// If data is not set we can send request to any worker
// first we try to pick idle worker, if not we randomly pick a worker
if ($data === null) {
for ($i = 0; $i < $totalWorkers; $i++) {
if ($server->getWorkerStatus($i) === SWOOLE_WORKER_IDLE) {
return $i;
}
}
return rand(0, $totalWorkers - 1);
}
$riskyWorkersPercent = intval(System::getEnv('_APP_RISKY_WORKERS_PERCENT', 80)) / 100; // Decimal form 0 to 1
// Each worker has numeric ID, starting from 0 and incrementing
// From 0 to riskyWorkers, we consider safe workers
// From riskyWorkers to totalWorkers, we consider risky workers
$riskyWorkers = (int) floor($totalWorkers * $riskyWorkersPercent); // Absolute amount of risky workers
$domain = '';
// max up to 3 as first line has request details and second line has host
$lines = explode("\n", $data, 3);
$request = $lines[0];
if (count($lines) > 1) {
$domain = trim(explode('Host: ', $lines[1])[1]);
}
// Sync executions are considered risky
$risky = false;
if (str_starts_with($request, 'POST') && str_contains($request, '/executions')) {
$risky = true;
} elseif (str_ends_with($domain, System::getEnv('_APP_DOMAIN_FUNCTIONS'))) {
$risky = true;
} elseif ($domains->get(md5($domain), 'value') === 1) {
// executions request coming from custom domain
$risky = true;
}
if ($risky) {
// If risky request, only consider risky workers
for ($j = $riskyWorkers; $j < $totalWorkers; $j++) {
/** Reference https://openswoole.com/docs/modules/swoole-server-getWorkerStatus#description */
if ($server->getWorkerStatus($j) === SWOOLE_WORKER_IDLE) {
// If idle worker found, give to him
return $j;
}
}
// If no idle workers, give to random risky worker
$worker = rand($riskyWorkers, $totalWorkers - 1);
Console::warning("swoole_dispatch: Risky branch: did not find a idle worker, picking random worker {$worker}");
return $worker;
}
// If safe request, give to any idle worker
// Its fine to pick risky worker here, because it's idle. Idle is never actually risky
for ($i = 0; $i < $totalWorkers; $i++) {
if ($server->getWorkerStatus($i) === SWOOLE_WORKER_IDLE) {
return $i;
}
}
// If no idle worker found, give to random safe worker
// We avoid risky workers here, as it could be in work - not idle. Thats exactly when they are risky.
$worker = rand(0, $riskyWorkers - 1);
Console::warning("swoole_dispatch: Non-risky branch: did not find a idle worker, picking random worker {$worker}");
return $worker;
}
include __DIR__ . '/controllers/general.php';
$http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $register) {
@@ -74,8 +172,8 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
do {
try {
$attempts++;
$dbForConsole = $app->getResource('dbForConsole');
/** @var Utopia\Database\Database $dbForConsole */
$dbForPlatform = $app->getResource('dbForPlatform');
/** @var Utopia\Database\Database $dbForPlatform */
break; // leave the do-while if successful
} catch (\Throwable $e) {
Console::warning("Database not ready. Retrying connection ({$attempts})...");
@@ -89,22 +187,17 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
Console::success('[Setup] - Server database init started...');
try {
Console::success('[Setup] - Creating database: appwrite...');
$dbForConsole->create();
} catch (\Throwable $e) {
Console::success('[Setup] - Creating console database...');
$dbForPlatform->create();
} catch (Duplicate) {
Console::success('[Setup] - Skip: metadata table already exists');
}
if ($dbForConsole->getCollection(Audit::COLLECTION)->isEmpty()) {
$audit = new Audit($dbForConsole);
if ($dbForPlatform->getCollection(Audit::COLLECTION)->isEmpty()) {
$audit = new Audit($dbForPlatform);
$audit->setup();
}
if ($dbForConsole->getCollection(TimeLimit::COLLECTION)->isEmpty()) {
$adapter = new TimeLimit("", 0, 1, $dbForConsole);
$adapter->setup();
}
/** @var array $collections */
$collections = Config::getParam('collections', []);
$consoleCollections = $collections['console'];
@@ -112,45 +205,21 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
if (($collection['$collection'] ?? '') !== Database::METADATA) {
continue;
}
if (!$dbForConsole->getCollection($key)->isEmpty()) {
if (!$dbForPlatform->getCollection($key)->isEmpty()) {
continue;
}
Console::success('[Setup] - Creating collection: ' . $collection['$id'] . '...');
Console::success('[Setup] - Creating console collection: ' . $collection['$id'] . '...');
$attributes = [];
$indexes = [];
$attributes = \array_map(fn ($attribute) => new Document($attribute), $collection['attributes']);
$indexes = \array_map(fn (array $index) => new Document($index), $collection['indexes']);
foreach ($collection['attributes'] as $attribute) {
$attributes[] = new Document([
'$id' => ID::custom($attribute['$id']),
'type' => $attribute['type'],
'size' => $attribute['size'],
'required' => $attribute['required'],
'signed' => $attribute['signed'],
'array' => $attribute['array'],
'filters' => $attribute['filters'],
'default' => $attribute['default'] ?? null,
'format' => $attribute['format'] ?? ''
]);
}
foreach ($collection['indexes'] as $index) {
$indexes[] = new Document([
'$id' => ID::custom($index['$id']),
'type' => $index['type'],
'attributes' => $index['attributes'],
'lengths' => $index['lengths'],
'orders' => $index['orders'],
]);
}
$dbForConsole->createCollection($key, $attributes, $indexes);
$dbForPlatform->createCollection($key, $attributes, $indexes);
}
if ($dbForConsole->getDocument('buckets', 'default')->isEmpty() && !$dbForConsole->exists($dbForConsole->getDatabase(), 'bucket_1')) {
if ($dbForPlatform->getDocument('buckets', 'default')->isEmpty() && !$dbForPlatform->exists($dbForPlatform->getDatabase(), 'bucket_1')) {
Console::success('[Setup] - Creating default bucket...');
$dbForConsole->createDocument('buckets', new Document([
$dbForPlatform->createDocument('buckets', new Document([
'$id' => ID::custom('default'),
'$collection' => ID::custom('buckets'),
'name' => 'Default',
@@ -170,7 +239,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
'search' => 'buckets Default',
]));
$bucket = $dbForConsole->getDocument('buckets', 'default');
$bucket = $dbForPlatform->getDocument('buckets', 'default');
Console::success('[Setup] - Creating files collection for default bucket...');
$files = $collections['buckets']['files'] ?? [];
@@ -178,34 +247,53 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
throw new Exception('Files collection is not configured.');
}
$attributes = [];
$indexes = [];
$attributes = \array_map(fn ($attribute) => new Document($attribute), $files['attributes']);
$indexes = \array_map(fn (array $index) => new Document($index), $files['indexes']);
foreach ($files['attributes'] as $attribute) {
$attributes[] = new Document([
'$id' => ID::custom($attribute['$id']),
'type' => $attribute['type'],
'size' => $attribute['size'],
'required' => $attribute['required'],
'signed' => $attribute['signed'],
'array' => $attribute['array'],
'filters' => $attribute['filters'],
'default' => $attribute['default'] ?? null,
'format' => $attribute['format'] ?? ''
]);
$dbForPlatform->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes);
}
$projectCollections = $collections['projects'];
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
$sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', ''));
$sharedTablesV2 = \array_diff($sharedTables, $sharedTablesV1);
$cache = $app->getResource('cache');
foreach ($sharedTablesV2 as $hostname) {
$adapter = $pools
->get($hostname)
->pop()
->getResource();
$dbForProject = (new Database($adapter, $cache))
->setDatabase('appwrite')
->setSharedTables(true)
->setTenant(null)
->setNamespace(System::getEnv('_APP_DATABASE_SHARED_NAMESPACE', ''));
try {
Console::success('[Setup] - Creating project database: ' . $hostname . '...');
$dbForProject->create();
} catch (Duplicate) {
Console::success('[Setup] - Skip: metadata table already exists');
}
foreach ($files['indexes'] as $index) {
$indexes[] = new Document([
'$id' => ID::custom($index['$id']),
'type' => $index['type'],
'attributes' => $index['attributes'],
'lengths' => $index['lengths'],
'orders' => $index['orders'],
]);
}
foreach ($projectCollections as $key => $collection) {
if (($collection['$collection'] ?? '') !== Database::METADATA) {
continue;
}
if (!$dbForProject->getCollection($key)->isEmpty()) {
continue;
}
$dbForConsole->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes);
$attributes = \array_map(fn ($attribute) => new Document($attribute), $collection['attributes']);
$indexes = \array_map(fn (array $index) => new Document($index), $collection['indexes']);
Console::success('[Setup] - Creating project collection: ' . $collection['$id'] . '...');
$dbForProject->createCollection($key, $attributes, $indexes);
}
}
$pools->reclaim();
@@ -216,6 +304,9 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
Console::success('Server started successfully (max payload is ' . number_format($payloadSize) . ' bytes)');
Console::info("Master pid {$http->master_pid}, manager pid {$http->manager_pid}");
// Start the task that starts fetching custom domains
$http->task([], 0);
// listen ctrl + c
Process::signal(2, function () use ($http) {
Console::log('Stop by Ctrl+C');
@@ -223,7 +314,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
});
});
$http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register) {
$http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register) {
App::setResource('swooleRequest', fn () => $swooleRequest);
App::setResource('swooleResponse', fn () => $swooleResponse);
@@ -272,10 +363,12 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
if (isset($user) && !$user->isEmpty()) {
$log->setUser(new User($user->getId()));
} else {
$log->setUser(new User('guest-' . hash('sha256', $request->getIP())));
}
$log->setNamespace("http");
$log->setServer(\gethostname());
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($th->getMessage());
@@ -295,6 +388,7 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
$action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD");
$log->setAction($action);
$log->addTag('service', $action);
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
@@ -333,4 +427,59 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
}
});
// Fetch domains every `DOMAIN_SYNC_TIMER` seconds and update in the memory
$http->on('Task', function () use ($register, $domains) {
$lastSyncUpdate = null;
$pools = $register->get('pools');
App::setResource('pools', fn () => $pools);
$app = new App('UTC');
/** @var Utopia\Database\Database $dbForPlatform */
$dbForPlatform = $app->getResource('dbForPlatform');
Console::loop(function () use ($dbForPlatform, $domains, &$lastSyncUpdate) {
try {
$time = DateTime::now();
$limit = 1000;
$sum = $limit;
$latestDocument = null;
while ($sum === $limit) {
$queries = [Query::limit($limit)];
if ($latestDocument !== null) {
$queries[] = Query::cursorAfter($latestDocument);
}
if ($lastSyncUpdate != null) {
$queries[] = Query::greaterThanEqual('$updatedAt', $lastSyncUpdate);
}
$queries[] = Query::equal('resourceType', ['function']);
$results = [];
try {
$results = Authorization::skip(fn () => $dbForPlatform->find('rules', $queries));
} catch (Throwable $th) {
Console::error($th->getMessage());
}
$sum = count($results);
foreach ($results as $document) {
$domain = $document->getAttribute('domain');
if (str_ends_with($domain, System::getEnv('_APP_DOMAIN_FUNCTIONS'))) {
continue;
}
$domains->set(md5($domain), ['value' => 1]);
}
$latestDocument = !empty(array_key_last($results)) ? $results[array_key_last($results)] : null;
}
$lastSyncUpdate = $time;
if ($sum > 0) {
Console::log("Sync domains tick: {$sum} domains were updated");
}
} catch (Throwable $th) {
Console::error($th->getMessage());
}
}, DOMAIN_SYNC_TIMER, 0, function ($error) {
Console::error($error);
});
});
$http->start();
+82 -41
View File
@@ -49,6 +49,7 @@ use Appwrite\Utopia\Request;
use MaxMind\Db\Reader;
use PHPMailer\PHPMailer\PHPMailer;
use Swoole\Database\PDOProxy;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\App;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Cache\Adapter\Sharding;
@@ -124,7 +125,7 @@ const APP_USER_ACCESS = 24 * 60 * 60; // 24 hours
const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 4318;
const APP_VERSION_STABLE = '1.6.0';
const APP_VERSION_STABLE = '1.6.1';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
@@ -232,6 +233,11 @@ const API_KEY_DYNAMIC = 'dynamic';
// Usage metrics
const METRIC_TEAMS = 'teams';
const METRIC_USERS = 'users';
const METRIC_WEBHOOKS_SENT = 'webhooks.events.sent';
const METRIC_WEBHOOKS_FAILED = 'webhooks.events.failed';
const METRIC_WEBHOOK_ID_SENT = '{webhookInternalId}.webhooks.events.sent';
const METRIC_WEBHOOK_ID_FAILED = '{webhookInternalId}.webhooks.events.failed';
const METRIC_AUTH_METHOD_PHONE = 'auth.method.phone';
const METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE = METRIC_AUTH_METHOD_PHONE . '.{countryCode}';
@@ -254,9 +260,15 @@ const METRIC_DOCUMENTS = 'documents';
const METRIC_DATABASE_ID_DOCUMENTS = '{databaseInternalId}.documents';
const METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS = '{databaseInternalId}.{collectionInternalId}.documents';
const METRIC_DATABASE_ID_COLLECTION_ID_STORAGE = '{databaseInternalId}.{collectionInternalId}.databases.storage';
const METRIC_DATABASES_OPERATIONS_READS = 'databases.operations.reads';
const METRIC_DATABASE_ID_OPERATIONS_READS = '{databaseInternalId}.databases.operations.reads';
const METRIC_DATABASES_OPERATIONS_WRITES = 'databases.operations.writes';
const METRIC_DATABASE_ID_OPERATIONS_WRITES = '{databaseInternalId}.databases.operations.writes';
const METRIC_BUCKETS = 'buckets';
const METRIC_FILES = 'files';
const METRIC_FILES_STORAGE = 'files.storage';
const METRIC_FILES_TRANSFORMATIONS = 'files.transformations';
const METRIC_BUCKET_ID_FILES_TRANSFORMATIONS = '{bucketInternalId}.files.transformations';
const METRIC_BUCKET_ID_FILES = '{bucketInternalId}.files';
const METRIC_BUCKET_ID_FILES_STORAGE = '{bucketInternalId}.files.storage';
const METRIC_FUNCTIONS = 'functions';
@@ -854,31 +866,31 @@ $register->set('pools', function () {
$connections = [
'console' => [
'type' => 'database',
'dsns' => System::getEnv('_APP_CONNECTIONS_DB_CONSOLE', $fallbackForDB),
'dsns' => $fallbackForDB,
'multiple' => false,
'schemes' => ['mariadb', 'mysql'],
],
'database' => [
'type' => 'database',
'dsns' => System::getEnv('_APP_CONNECTIONS_DB_PROJECT', $fallbackForDB),
'dsns' => $fallbackForDB,
'multiple' => true,
'schemes' => ['mariadb', 'mysql'],
],
'queue' => [
'type' => 'queue',
'dsns' => System::getEnv('_APP_CONNECTIONS_QUEUE', $fallbackForRedis),
'dsns' => $fallbackForRedis,
'multiple' => false,
'schemes' => ['redis'],
],
'pubsub' => [
'type' => 'pubsub',
'dsns' => System::getEnv('_APP_CONNECTIONS_PUBSUB', $fallbackForRedis),
'dsns' => $fallbackForRedis,
'multiple' => false,
'schemes' => ['redis'],
],
'cache' => [
'type' => 'cache',
'dsns' => System::getEnv('_APP_CONNECTIONS_CACHE', $fallbackForRedis),
'dsns' => $fallbackForRedis,
'multiple' => true,
'schemes' => ['redis'],
],
@@ -890,7 +902,7 @@ $register->set('pools', function () {
$multiprocessing = System::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled';
if ($multiprocessing) {
$workerCount = swoole_cpu_num() * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
} else {
$workerCount = 1;
}
@@ -950,12 +962,12 @@ $register->set('pools', function () {
});
},
'redis' => function () use ($dsnHost, $dsnPort, $dsnPass) {
$redis = new Redis();
$redis = new \Redis();
@$redis->pconnect($dsnHost, (int)$dsnPort);
if ($dsnPass) {
$redis->auth($dsnPass);
}
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
$redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
return $redis;
},
@@ -1215,12 +1227,12 @@ App::setResource('clients', function ($request, $console, $project) {
return \array_unique($clients);
}, ['request', 'console', 'project']);
App::setResource('user', function ($mode, $project, $console, $request, $response, $dbForProject, $dbForConsole) {
App::setResource('user', function ($mode, $project, $console, $request, $response, $dbForProject, $dbForPlatform) {
/** @var Appwrite\Utopia\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Database\Database $dbForConsole */
/** @var Utopia\Database\Database $dbForPlatform */
/** @var string $mode */
Authorization::setDefaultStatus(true);
@@ -1269,13 +1281,13 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
$user = new Document([]);
} else {
if ($project->getId() === 'console') {
$user = $dbForConsole->getDocument('users', Auth::$unique);
$user = $dbForPlatform->getDocument('users', Auth::$unique);
} else {
$user = $dbForProject->getDocument('users', Auth::$unique);
}
}
} else {
$user = $dbForConsole->getDocument('users', Auth::$unique);
$user = $dbForPlatform->getDocument('users', Auth::$unique);
}
if (
@@ -1318,14 +1330,14 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
}
$dbForProject->setMetadata('user', $user->getId());
$dbForConsole->setMetadata('user', $user->getId());
$dbForPlatform->setMetadata('user', $user->getId());
return $user;
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForConsole']);
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform']);
App::setResource('project', function ($dbForConsole, $request, $console) {
App::setResource('project', function ($dbForPlatform, $request, $console) {
/** @var Appwrite\Utopia\Request $request */
/** @var Utopia\Database\Database $dbForConsole */
/** @var Utopia\Database\Database $dbForPlatform */
/** @var Utopia\Database\Document $console */
$projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', ''));
@@ -1334,10 +1346,10 @@ App::setResource('project', function ($dbForConsole, $request, $console) {
return $console;
}
$project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId));
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
return $project;
}, ['dbForConsole', 'request', 'console']);
}, ['dbForPlatform', 'request', 'console']);
App::setResource('session', function (Document $user) {
if ($user->isEmpty()) {
@@ -1402,9 +1414,9 @@ App::setResource('console', function () {
]);
}, []);
App::setResource('dbForProject', function (Group $pools, Database $dbForConsole, Cache $cache, Document $project) {
App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForConsole;
return $dbForPlatform;
}
try {
@@ -1427,14 +1439,9 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForConsole,
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
try {
$dsn = new DSN($project->getAttribute('database'));
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -1447,9 +1454,9 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForConsole,
}
return $database;
}, ['pools', 'dbForConsole', 'cache', 'project']);
}, ['pools', 'dbForPlatform', 'cache', 'project']);
App::setResource('dbForConsole', function (Group $pools, Cache $cache) {
App::setResource('dbForPlatform', function (Group $pools, Cache $cache) {
$dbAdapter = $pools
->get('console')
->pop()
@@ -1467,12 +1474,12 @@ App::setResource('dbForConsole', function (Group $pools, Cache $cache) {
return $database;
}, ['pools', 'cache']);
App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) {
App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) {
return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForConsole;
return $dbForPlatform;
}
try {
@@ -1489,7 +1496,9 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole,
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -1519,7 +1528,7 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole,
return $database;
};
}, ['pools', 'dbForConsole', 'cache']);
}, ['pools', 'dbForPlatform', 'cache']);
App::setResource('cache', function (Group $pools) {
$list = Config::getParam('pools-cache', []);
@@ -1536,6 +1545,27 @@ App::setResource('cache', function (Group $pools) {
return new Cache(new Sharding($adapters));
}, ['pools']);
App::setResource('redis', function () {
$host = System::getEnv('_APP_REDIS_HOST', 'localhost');
$port = System::getEnv('_APP_REDIS_PORT', 6379);
$pass = System::getEnv('_APP_REDIS_PASS', '');
$redis = new \Redis();
@$redis->pconnect($host, (int)$port);
if ($pass) {
$redis->auth($pass);
}
$redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
return $redis;
});
App::setResource('timelimit', function (\Redis $redis) {
return function (string $key, int $limit, int $time) use ($redis) {
return new TimeLimitRedis($key, $limit, $time, $redis);
};
}, ['redis']);
App::setResource('deviceForLocal', function () {
return new Local();
});
@@ -1860,7 +1890,7 @@ App::setResource('plan', function (array $plan = []) {
return [];
});
App::setResource('team', function (Document $project, Database $dbForConsole, App $utopia, Request $request) {
App::setResource('team', function (Document $project, Database $dbForPlatform, App $utopia, Request $request) {
$teamInternalId = '';
if ($project->getId() !== 'console') {
$teamInternalId = $project->getAttribute('teamInternalId', '');
@@ -1870,25 +1900,36 @@ App::setResource('team', function (Document $project, Database $dbForConsole, Ap
if (str_starts_with($path, '/v1/projects/:projectId')) {
$uri = $request->getURI();
$pid = explode('/', $uri)[3];
$p = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $pid));
$p = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $pid));
$teamInternalId = $p->getAttribute('teamInternalId', '');
} elseif ($path === '/v1/projects') {
$teamId = $request->getParam('teamId', '');
$team = Authorization::skip(fn () => $dbForConsole->getDocument('teams', $teamId));
$team = Authorization::skip(fn () => $dbForPlatform->getDocument('teams', $teamId));
return $team;
}
}
$team = Authorization::skip(function () use ($dbForConsole, $teamInternalId) {
return $dbForConsole->findOne('teams', [
$team = Authorization::skip(function () use ($dbForPlatform, $teamInternalId) {
return $dbForPlatform->findOne('teams', [
Query::equal('$internalId', [$teamInternalId]),
]);
});
return $team;
}, ['project', 'dbForConsole', 'utopia', 'request']);
}, ['project', 'dbForPlatform', 'utopia', 'request']);
App::setResource(
'isResourceBlocked',
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
);
App::setResource('previewHostname', function (Request $request) {
if (App::isDevelopment()) {
$host = $request->getQuery('appwrite-hostname') ?? '';
if (!empty($host)) {
return $host;
}
}
return '';
}, ['request']);
+63 -13
View File
@@ -13,7 +13,7 @@ use Swoole\Runtime;
use Swoole\Table;
use Swoole\Timer;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\Database\TimeLimit;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\App;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
@@ -29,6 +29,7 @@ use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\System\System;
use Utopia\Telemetry\Adapter\None as NoTelemetry;
use Utopia\WebSocket\Adapter;
use Utopia\WebSocket\Server;
@@ -92,7 +93,9 @@ if (!function_exists('getProjectDB')) {
$database = new Database($adapter, getCache());
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -135,6 +138,32 @@ if (!function_exists('getCache')) {
}
}
// Allows overriding
if (!function_exists('getRedis')) {
function getRedis(): \Redis
{
$host = System::getEnv('_APP_REDIS_HOST', 'localhost');
$port = System::getEnv('_APP_REDIS_PORT', 6379);
$pass = System::getEnv('_APP_REDIS_PASS', '');
$redis = new \Redis();
@$redis->pconnect($host, (int)$port);
if ($pass) {
$redis->auth($pass);
}
$redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
return $redis;
}
}
if (!function_exists('getTimelimit')) {
function getTimelimit(): TimeLimitRedis
{
return new TimeLimitRedis("", 0, 1, getRedis());
}
}
if (!function_exists('getRealtime')) {
function getRealtime(): Realtime
{
@@ -142,6 +171,13 @@ if (!function_exists('getRealtime')) {
}
}
if (!function_exists('getTelemetry')) {
function getTelemetry(int $workerId): Utopia\Telemetry\Adapter
{
return new NoTelemetry();
}
}
$realtime = getRealtime();
/**
@@ -157,7 +193,7 @@ $stats->create();
$containerId = uniqid();
$statsDocument = null;
$workerNumber = swoole_cpu_num() * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$workerNumber = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$adapter = new Adapter\Swoole(port: System::getEnv('PORT', 80));
$adapter
@@ -174,7 +210,7 @@ $logError = function (Throwable $error, string $action) use ($register) {
$log = new Log();
$log->setNamespace("realtime");
$log->setServer(gethostname());
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
@@ -274,6 +310,12 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime, $logError) {
Console::success('Worker ' . $workerId . ' started successfully');
$telemetry = getTelemetry($workerId);
$register->set('telemetry', fn () => $telemetry);
$register->set('telemetry.connectionCounter', fn () => $telemetry->createUpDownCounter('realtime.server.open_connections'));
$register->set('telemetry.connectionCreatedCounter', fn () => $telemetry->createCounter('realtime.server.connection.created'));
$register->set('telemetry.messageSentCounter', fn () => $telemetry->createCounter('realtime.server.message.sent'));
$attempts = 0;
$start = time();
@@ -416,6 +458,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
);
if (($num = count($receivers)) > 0) {
$register->get('telemetry.messageSentCounter')->add($num);
$stats->incr($event['project'], 'messages', $num);
}
});
@@ -464,7 +507,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
$dbForProject = getProjectDB($project);
$timelimit = $app->getResource('timelimit');
$console = $app->getResource('console'); /** @var Document $console */
$user = $app->getResource('user'); /** @var Document $user */
@@ -473,12 +516,12 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
*
* Abuse limits are connecting 128 times per minute and ip address.
*/
$timeLimit = new TimeLimit('url:{url},ip:{ip}', 128, 60, $dbForProject);
$timeLimit
$timelimit = $timelimit('url:{url},ip:{ip}', 128, 60);
$timelimit
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getURI());
$abuse = new Abuse($timeLimit);
$abuse = new Abuse($timelimit);
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled' && $abuse->check()) {
throw new Exception(Exception::REALTIME_TOO_MANY_MESSAGES, 'Too many requests');
@@ -519,6 +562,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
]
]));
$register->get('telemetry.connectionCounter')->add(1);
$register->get('telemetry.connectionCreatedCounter')->add(1);
$stats->set($project->getId(), [
'projectId' => $project->getId(),
'teamId' => $project->getAttribute('teamId')
@@ -573,7 +619,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
*
* Abuse limits are sending 32 times per minute and connection.
*/
$timeLimit = new TimeLimit('url:{url},connection:{connection}', 32, 60, $database);
$timeLimit = getTimelimit('url:{url},connection:{connection}', 32, 60);
$timeLimit
->setParam('{connection}', $connection)
@@ -592,9 +638,12 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
}
switch ($message['type']) {
/**
* This type is used to authenticate.
*/
case 'ping':
$server->send([$connection], json_encode([
'type' => 'pong'
]));
break;
case 'authentication':
if (!array_key_exists('session', $message['data'])) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
@@ -652,9 +701,10 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
}
});
$server->onClose(function (int $connection) use ($realtime, $stats) {
$server->onClose(function (int $connection) use ($realtime, $stats, $register) {
if (array_key_exists($connection, $realtime->connections)) {
$stats->decr($realtime->connections[$connection]['projectId'], 'connectionsTotal');
$register->get('telemetry.connectionCounter')->add(-1);
}
$realtime->unsubscribe($connection);
+1 -1
View File
@@ -167,7 +167,7 @@ $image = $this->getParam('image', '');
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: <?php echo $organization; ?>/console:5.0.12
image: <?php echo $organization; ?>/console:5.2.27
restart: unless-stopped
networks:
- appwrite
+98 -17
View File
@@ -2,6 +2,7 @@
require_once __DIR__ . '/init.php';
use Appwrite\Certificates\LetsEncrypt;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
@@ -16,7 +17,7 @@ use Appwrite\Event\Usage;
use Appwrite\Event\UsageDump;
use Appwrite\Platform\Appwrite;
use Swoole\Runtime;
use Utopia\App;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
@@ -41,7 +42,7 @@ Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
Server::setResource('register', fn () => $register);
Server::setResource('dbForConsole', function (Cache $cache, Registry $register) {
Server::setResource('dbForPlatform', function (Cache $cache, Registry $register) {
$pools = $register->get('pools');
$database = $pools
->get('console')
@@ -54,7 +55,7 @@ Server::setResource('dbForConsole', function (Cache $cache, Registry $register)
return $adapter;
}, ['cache', 'register']);
Server::setResource('project', function (Message $message, Database $dbForConsole) {
Server::setResource('project', function (Message $message, Database $dbForPlatform) {
$payload = $message->getPayload() ?? [];
$project = new Document($payload['project'] ?? []);
@@ -62,12 +63,12 @@ Server::setResource('project', function (Message $message, Database $dbForConsol
return $project;
}
return $dbForConsole->getDocument('projects', $project->getId());
}, ['message', 'dbForConsole']);
return $dbForPlatform->getDocument('projects', $project->getId());
}, ['message', 'dbForPlatform']);
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForConsole) {
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForPlatform) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForConsole;
return $dbForPlatform;
}
$pools = $register->get('pools');
@@ -93,7 +94,9 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -106,14 +109,14 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
}
return $database;
}, ['cache', 'register', 'message', 'project', 'dbForConsole']);
}, ['cache', 'register', 'message', 'project', 'dbForPlatform']);
Server::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) {
Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases): Database {
return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases): Database {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForConsole;
return $dbForPlatform;
}
try {
@@ -126,7 +129,9 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForConso
if (isset($databases[$dsn->getHost()])) {
$database = $databases[$dsn->getHost()];
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -150,7 +155,9 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForConso
$databases[$dsn->getHost()] = $database;
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -164,10 +171,10 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForConso
return $database;
};
}, ['pools', 'dbForConsole', 'cache']);
}, ['pools', 'dbForPlatform', 'cache']);
Server::setResource('abuseRetention', function () {
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400));
return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400);
});
Server::setResource('auditRetention', function () {
@@ -194,6 +201,27 @@ Server::setResource('cache', function (Registry $register) {
return new Cache(new Sharding($adapters));
}, ['register']);
Server::setResource('redis', function () {
$host = System::getEnv('_APP_REDIS_HOST', 'localhost');
$port = System::getEnv('_APP_REDIS_PORT', 6379);
$pass = System::getEnv('_APP_REDIS_PASS', '');
$redis = new \Redis();
@$redis->pconnect($host, (int)$port);
if ($pass) {
$redis->auth($pass);
}
$redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
return $redis;
});
Server::setResource('timelimit', function (\Redis $redis) {
return function (string $key, int $limit, int $time) use ($redis) {
return new TimeLimitRedis($key, $limit, $time, $redis);
};
}, ['redis']);
Server::setResource('log', fn () => new Log());
Server::setResource('queueForUsage', function (Connection $queue) {
@@ -277,6 +305,59 @@ Server::setResource(
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
);
Server::setResource('certificates', function () {
$email = System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'));
if (empty($email)) {
throw new Exception('You must set a valid security email address (_APP_EMAIL_CERTIFICATES) to issue a LetsEncrypt SSL certificate.');
}
return new LetsEncrypt($email);
});
Server::setResource('logError', function (Registry $register, Document $project) {
return function (Throwable $error, string $namespace, string $action, ?array $extras) use ($register, $project) {
$logger = $register->get('logger');
if ($logger) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$log = new Log();
$log->setNamespace($namespace);
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
$log->addTag('code', $error->getCode());
$log->addTag('verboseType', get_class($error));
$log->addTag('projectId', $project->getId() ?? '');
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
foreach ($extras as $key => $value) {
$log->addExtra($key, $value);
}
$log->setAction($action);
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
try {
$responseCode = $logger->addLog($log);
Console::info('Error log pushed with status code: ' . $responseCode);
} catch (Throwable $th) {
Console::error('Error pushing log: ' . $th->getMessage());
}
}
Console::warning("Failed: {$error->getMessage()}");
Console::warning($error->getTraceAsString());
};
}, ['register', 'project']);
$pools = $register->get('pools');
$platform = new Appwrite();
@@ -335,7 +416,7 @@ $worker
if ($logger) {
$log->setNamespace("appwrite-worker");
$log->setServer(\gethostname());
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
+9 -4
View File
@@ -45,13 +45,13 @@
"ext-sockets": "*",
"appwrite/php-runtimes": "0.16.*",
"appwrite/php-clamav": "2.0.*",
"utopia-php/abuse": "0.43.0",
"utopia-php/abuse": "0.46.*",
"utopia-php/analytics": "0.10.*",
"utopia-php/audit": "0.43.0",
"utopia-php/audit": "0.43.*",
"utopia-php/cache": "0.11.*",
"utopia-php/cli": "0.15.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.53.16",
"utopia-php/database": "0.53.200",
"utopia-php/domains": "0.5.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/framework": "0.33.*",
@@ -70,6 +70,7 @@
"utopia-php/storage": "0.18.*",
"utopia-php/swoole": "0.8.*",
"utopia-php/system": "0.9.*",
"utopia-php/telemetry": "0.1.*",
"utopia-php/vcs": "0.8.*",
"utopia-php/websocket": "0.1.*",
"matomo/device-detector": "6.1.*",
@@ -83,7 +84,7 @@
},
"require-dev": {
"ext-fileinfo": "*",
"appwrite/sdk-generator": "0.39.*",
"appwrite/sdk-generator": "0.39.28",
"phpunit/phpunit": "9.5.20",
"swoole/ide-helper": "5.1.2",
"textalk/websocket": "1.5.7",
@@ -96,6 +97,10 @@
"config": {
"platform": {
"php": "8.3"
},
"allow-plugins": {
"php-http/discovery": false,
"tbachert/spi": false
}
}
}
Generated
+1952 -448
View File
File diff suppressed because it is too large Load Diff
+7 -2
View File
@@ -194,11 +194,14 @@ services:
- _APP_EXPERIMENT_LOGGING_CONFIG
- _APP_DATABASE_SHARED_TABLES
- _APP_GEO_SECRET
- _APP_DATABASE_SHARED_TABLES_V1
- _APP_DATABASE_SHARED_NAMESPACE
- _APP_FUNCTIONS_CREATION_ABUSE_LIMIT
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: appwrite/console:5.0.12
image: appwrite/console:5.2.27
restart: unless-stopped
networks:
- appwrite
@@ -383,6 +386,8 @@ services:
- _APP_EXECUTOR_SECRET
- _APP_EXECUTOR_HOST
- _APP_DATABASE_SHARED_TABLES
- _APP_DATABASE_SHARED_TABLES_V1
- _APP_EMAIL_CERTIFICATES
appwrite-worker-databases:
entrypoint: worker-databases
@@ -865,7 +870,7 @@ services:
appwrite-assistant:
container_name: appwrite-assistant
image: appwrite/assistant:0.5.0
image: appwrite/assistant:0.7.0
networks:
- appwrite
environment:
@@ -12,5 +12,6 @@ mutation {
providerId
providerType
identifier
expired
}
}
@@ -33,6 +33,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -28,6 +28,7 @@ query {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -31,6 +31,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -30,6 +30,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -31,6 +31,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -30,6 +30,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -31,6 +31,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -31,6 +31,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -30,6 +30,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -11,5 +11,6 @@ mutation {
providerId
providerType
identifier
expired
}
}
@@ -28,6 +28,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -17,6 +17,7 @@ mutation {
providerId
providerType
identifier
expired
}
userId
userName
@@ -1,3 +1,3 @@
Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST /v1/account/sessions/token](https://appwrite.io/docs/references/cloud/client-web/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.
Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST /v1/account/sessions/token](https://appwrite.io/docs/references/cloud/client-web/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.
A user is limited to 10 active sessions at a time by default. [Learn more about session limits](https://appwrite.io/docs/authentication-security#limits).
-389
View File
@@ -1,389 +0,0 @@
<?php
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
class Firebase extends OAuth2
{
/**
* @var array
*/
protected array $user = [];
/**
* @var array
*/
protected array $tokens = [];
/**
* @var array
*/
protected array $scopes = [
'https://www.googleapis.com/auth/firebase',
'https://www.googleapis.com/auth/datastore',
'https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/identitytoolkit',
'https://www.googleapis.com/auth/userinfo.profile'
];
/**
* @var array
*/
protected array $iamPermissions = [
// Database
'datastore.databases.get',
'datastore.databases.list',
'datastore.entities.get',
'datastore.entities.list',
'datastore.indexes.get',
'datastore.indexes.list',
// Generic Firebase permissions
'firebase.projects.get',
// Auth
'firebaseauth.configs.get',
'firebaseauth.configs.getHashConfig',
'firebaseauth.configs.getSecret',
'firebaseauth.users.get',
'identitytoolkit.tenants.get',
'identitytoolkit.tenants.list',
// IAM Assignment
'iam.serviceAccounts.list',
// Storage
'storage.buckets.get',
'storage.buckets.list',
'storage.objects.get',
'storage.objects.list'
];
/**
* @return string
*/
public function getName(): string
{
return 'firebase';
}
/**
* @return string
*/
public function getLoginURL(): string
{
return 'https://accounts.google.com/o/oauth2/v2/auth?' . \http_build_query([
'access_type' => 'offline',
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'scope' => \implode(' ', $this->getScopes()),
'state' => \json_encode($this->state),
'response_type' => 'code',
'prompt' => 'consent',
]);
}
/**
* @param string $code
*
* @return array
*/
protected function getTokens(string $code): array
{
if (empty($this->tokens)) {
$response = $this->request(
'POST',
'https://oauth2.googleapis.com/token',
[],
\http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'client_secret' => $this->appSecret,
'code' => $code,
'grant_type' => 'authorization_code'
])
);
$this->tokens = \json_decode($response, true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken): array
{
$response = $this->request(
'POST',
'https://oauth2.googleapis.com/token',
[],
\http_build_query([
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken
])
);
$output = [];
\parse_str($response, $output);
$this->tokens = $output;
if (empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return $this->tokens;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserID(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['id'] ?? '';
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserEmail(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['email'] ?? '';
}
/**
* Check if the OAuth email is verified
*
* @link https://docs.github.com/en/rest/users/emails#list-email-addresses-for-the-authenticated-user
*
* @param string $accessToken
*
* @return bool
*/
public function isEmailVerified(string $accessToken): bool
{
$user = $this->getUser($accessToken);
if ($user['verified'] ?? false) {
return true;
}
return false;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserName(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['name'] ?? '';
}
/**
* @param string $accessToken
*
* @return array
*/
protected function getUser(string $accessToken)
{
if (empty($this->user)) {
$response = $this->request(
'GET',
'https://www.googleapis.com/oauth2/v1/userinfo?access_token=' . \urlencode($accessToken),
[],
);
$this->user = \json_decode($response, true);
}
return $this->user;
}
public function getProjects(string $accessToken): array
{
$projects = $this->request('GET', 'https://firebase.googleapis.com/v1beta1/projects', ['Authorization: Bearer ' . \urlencode($accessToken)]);
$projects = \json_decode($projects, true);
return $projects['results'];
}
/*
Be careful with the setIAMPolicy method, it will overwrite all existing policies
**/
public function assignIAMRole(string $accessToken, string $email, string $projectId, array $role)
{
// Get IAM Roles
$iamRoles = $this->request('POST', 'https://cloudresourcemanager.googleapis.com/v1/projects/' . $projectId . ':getIamPolicy', [
'Authorization: Bearer ' . \urlencode($accessToken),
'Content-Type: application/json'
]);
$iamRoles = \json_decode($iamRoles, true);
$iamRoles['bindings'][] = [
'role' => $role['name'],
'members' => [
'serviceAccount:' . $email
]
];
// Set IAM Roles
$this->request('POST', 'https://cloudresourcemanager.googleapis.com/v1/projects/' . $projectId . ':setIamPolicy', [
'Authorization: Bearer ' . \urlencode($accessToken),
'Content-Type: application/json'
], \json_encode([
'policy' => $iamRoles
]));
}
private function generateRandomString($length = 10): string
{
$characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[random_int(0, $charactersLength - 1)];
}
return $randomString;
}
private function createCustomRole(string $accessToken, string $projectId): array
{
// Check if role already exists
try {
$role = $this->request('GET', 'https://iam.googleapis.com/v1/projects/' . $projectId . '/roles/appwriteMigrations', [
'Content-Type: application/json',
'Authorization: Bearer ' . \urlencode($accessToken),
]);
$role = \json_decode($role, true);
return $role;
} catch (\Throwable $e) {
if ($e->getCode() !== 404) {
throw $e;
}
}
// Create role if doesn't exist or isn't correct
$role = $this->request(
'POST',
'https://iam.googleapis.com/v1/projects/' . $projectId . '/roles/',
[
'Content-Type: application/json',
'Authorization: Bearer ' . \urlencode($accessToken),
],
\json_encode(
[
'roleId' => 'appwriteMigrations',
'role' => [
'title' => 'Appwrite Migrations',
'description' => 'A helper role for Appwrite Migrations',
'includedPermissions' => $this->iamPermissions,
'stage' => 'GA'
]
]
)
);
return json_decode($role, true);
}
public function createServiceAccount(string $accessToken, string $projectId): array
{
// Create Service Account
$uid = $this->generateRandomString();
$response = $this->request(
'POST',
'https://iam.googleapis.com/v1/projects/' . $projectId . '/serviceAccounts',
[
'Authorization: Bearer ' . \urlencode($accessToken),
'Content-Type: application/json'
],
\json_encode([
'accountId' => 'appwrite-' . $uid,
'serviceAccount' => [
'displayName' => 'Appwrite Migrations ' . $uid
]
])
);
$response = json_decode($response, true);
// Create and assign IAM Roles
$role = $this->createCustomRole($accessToken, $projectId);
\sleep(1); // Wait for IAM to propagate changes.
$this->assignIAMRole($accessToken, $response['email'], $projectId, $role);
// Create Service Account Key
$responseKey = $this->request(
'POST',
'https://iam.googleapis.com/v1/projects/' . $projectId . '/serviceAccounts/' . $response['email'] . '/keys',
[
'Authorization: Bearer ' . \urlencode($accessToken),
'Content-Type: application/json'
]
);
$responseKey = json_decode($responseKey, true);
return json_decode(base64_decode($responseKey['privateKeyData']), true);
}
public function cleanupServiceAccounts(string $accessToken, string $projectId)
{
// List Service Accounts
$response = $this->request(
'GET',
'https://iam.googleapis.com/v1/projects/' . $projectId . '/serviceAccounts',
[
'Authorization: Bearer ' . \urlencode($accessToken),
'Content-Type: application/json'
]
);
$response = json_decode($response, true);
if (empty($response['accounts'])) {
return false;
}
foreach ($response['accounts'] as $account) {
if (strpos($account['email'], 'appwrite-') !== false) {
$this->request(
'DELETE',
'https://iam.googleapis.com/v1/projects/' . $projectId . '/serviceAccounts/' . $account['email'],
[
'Authorization: Bearer ' . \urlencode($accessToken),
'Content-Type: application/json'
]
);
}
}
return true;
}
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace Appwrite\Certificates;
use Utopia\Logger\Log;
interface Adapter
{
public function issueCertificate(string $certName, string $domain): ?string;
public function isRenewRequired(string $domain, Log $log): bool;
public function deleteCertificate(string $domain): void;
}
+125
View File
@@ -0,0 +1,125 @@
<?php
namespace Appwrite\Certificates;
use Exception;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\DateTime;
use Utopia\Logger\Log;
class LetsEncrypt implements Adapter
{
private string $email;
public function __construct(string $email)
{
$this->email = $email;
}
public function issueCertificate(string $certName, string $domain): ?string
{
$stdout = '';
$stderr = '';
$staging = (App::isProduction()) ? '' : ' --dry-run';
$exit = Console::execute(
"certbot certonly -v --webroot --noninteractive --agree-tos{$staging}"
. " --email " . $this->email
. " --cert-name " . $certName
. " -w " . APP_STORAGE_CERTIFICATES
. " -d {$domain}",
'',
$stdout,
$stderr
);
// Unexpected error, usually 5XX, API limits, ...
if ($exit !== 0) {
throw new Exception('Failed to issue a certificate with message: ' . $stderr);
}
// Prepare folder in storage for domain
$path = APP_STORAGE_CERTIFICATES . '/' . $domain;
if (!\is_readable($path)) {
if (!\mkdir($path, 0755, true)) {
throw new Exception('Failed to create path for certificate.');
}
}
// Move generated files
if (!@\rename('/etc/letsencrypt/live/' . $certName . '/cert.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem')) {
throw new Exception('Failed to rename certificate cert.pem. Let\'s Encrypt log: ' . $stderr . ' ; ' . $stdout);
}
if (!@\rename('/etc/letsencrypt/live/' . $certName . '/chain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/chain.pem')) {
throw new Exception('Failed to rename certificate chain.pem. Let\'s Encrypt log: ' . $stderr . ' ; ' . $stdout);
}
if (!@\rename('/etc/letsencrypt/live/' . $certName . '/fullchain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/fullchain.pem')) {
throw new Exception('Failed to rename certificate fullchain.pem. Let\'s Encrypt log: ' . $stderr . ' ; ' . $stdout);
}
if (!@\rename('/etc/letsencrypt/live/' . $certName . '/privkey.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/privkey.pem')) {
throw new Exception('Failed to rename certificate privkey.pem. Let\'s Encrypt log: ' . $stderr . ' ; ' . $stdout);
}
$config = \implode(PHP_EOL, [
"tls:",
" certificates:",
" - certFile: /storage/certificates/{$domain}/fullchain.pem",
" keyFile: /storage/certificates/{$domain}/privkey.pem"
]);
// Save configuration into Traefik using our new cert files
if (!\file_put_contents(APP_STORAGE_CONFIG . '/' . $domain . '.yml', $config)) {
throw new Exception('Failed to save Traefik configuration.');
}
$certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem';
$certData = openssl_x509_parse(file_get_contents($certPath));
$validTo = $certData['validTo_time_t'] ?? null;
$dt = (new \DateTime())->setTimestamp($validTo);
return DateTime::addSeconds($dt, -60 * 60 * 24 * 30);
}
public function isRenewRequired(string $domain, Log $log): bool
{
$certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem';
if (\file_exists($certPath)) {
$certData = openssl_x509_parse(file_get_contents($certPath));
$validTo = $certData['validTo_time_t'] ?? 0;
if (empty($validTo)) {
$log->addTag('certificateDomain', $domain);
throw new Exception('Unable to read certificate file (cert.pem).');
}
// LetsEncrypt allows renewal 30 days before expiry
$expiryInAdvance = (60 * 60 * 24 * 30);
if ($validTo - $expiryInAdvance > \time()) {
$log->addTag('certificateDomain', $domain);
$log->addExtra('certificateData', \is_array($certData) ? \json_encode($certData) : \strval($certData));
return false;
}
}
return true;
}
public function deleteCertificate(string $domain): void
{
$directory = APP_STORAGE_CERTIFICATES . '/' . $domain;
$checkTraversal = realpath($directory) === $directory;
if ($checkTraversal && is_dir($directory)) {
// Delete files, so Traefik is aware of change
array_map('unlink', glob($directory . '/*.*'));
rmdir($directory);
Console::info("Deleted certificate files for {$domain}");
} else {
Console::info("No certificate files found for {$domain}");
}
}
}
+7 -1
View File
@@ -42,6 +42,7 @@ class Usage extends Event
*/
public function addMetric(string $key, int $value): self
{
$this->metrics[] = [
'key' => $key,
'value' => $value,
@@ -62,10 +63,15 @@ class Usage extends Event
}
$client = new Client($this->queue, $this->connection);
return $client->enqueue([
$result = $client->enqueue([
'project' => $this->getProject(),
'reduce' => $this->reduce,
'metrics' => $this->metrics,
]);
$this->metrics = [];
return $result;
}
}
+13
View File
@@ -2,6 +2,7 @@
namespace Appwrite\Event;
use Utopia\Database\Document;
use Utopia\Queue\Connection;
class Webhook extends Event
@@ -14,4 +15,16 @@ class Webhook extends Event
->setQueue(Event::WEBHOOK_QUEUE_NAME)
->setClass(Event::WEBHOOK_CLASS_NAME);
}
public function trigger(): string|bool
{
/** Filter out context and trim project to keep the payload small */
$this->context = [];
$this->project = new Document([
'$id' => $this->project->getId(),
'$internalId' => $this->project->getInternalId(),
]);
return parent::trigger();
}
}
+1
View File
@@ -91,6 +91,7 @@ abstract class Migration
'1.5.10' => 'V20',
'1.5.11' => 'V20',
'1.6.0' => 'V21',
'1.6.1' => 'V21',
];
/**
+11 -18
View File
@@ -23,13 +23,13 @@ class Maintenance extends Action
{
$this
->desc('Schedules maintenance tasks and publishes them to our queues')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForCertificates')
->inject('queueForDeletes')
->callback(fn (Database $dbForConsole, Certificate $queueForCertificates, Delete $queueForDeletes) => $this->action($dbForConsole, $queueForCertificates, $queueForDeletes));
->callback(fn (Database $dbForPlatform, Certificate $queueForCertificates, Delete $queueForDeletes) => $this->action($dbForPlatform, $queueForCertificates, $queueForDeletes));
}
public function action(Database $dbForConsole, Certificate $queueForCertificates, Delete $queueForDeletes): void
public function action(Database $dbForPlatform, Certificate $queueForCertificates, Delete $queueForDeletes): void
{
Console::title('Maintenance V1');
Console::success(APP_NAME . ' maintenance process v1 has started');
@@ -41,19 +41,19 @@ class Maintenance extends Action
$cacheRetention = (int) System::getEnv('_APP_MAINTENANCE_RETENTION_CACHE', '2592000'); // 30 days
$schedulesDeletionRetention = (int) System::getEnv('_APP_MAINTENANCE_RETENTION_SCHEDULES', '86400'); // 1 Day
Console::loop(function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForConsole, $queueForDeletes, $queueForCertificates) {
Console::loop(function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $queueForDeletes, $queueForCertificates) {
$time = DateTime::now();
Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds");
$this->foreachProject($dbForConsole, function (Document $project) use ($queueForDeletes, $usageStatsRetentionHourly) {
$this->foreachProject($dbForPlatform, function (Document $project) use ($queueForDeletes, $usageStatsRetentionHourly) {
$queueForDeletes->setProject($project);
$this->notifyProjects($queueForDeletes, $usageStatsRetentionHourly);
});
$this->notifyDeleteConnections($queueForDeletes);
$this->renewCertificates($dbForConsole, $queueForCertificates);
$this->renewCertificates($dbForPlatform, $queueForCertificates);
$this->notifyDeleteCache($cacheRetention, $queueForDeletes);
$this->notifyDeleteSchedules($schedulesDeletionRetention, $queueForDeletes);
}, $interval, $delay);
@@ -66,13 +66,12 @@ class Maintenance extends Action
{
$this->notifyDeleteTargets($queueForDeletes);
$this->notifyDeleteExecutionLogs($queueForDeletes);
$this->notifyDeleteAbuseLogs($queueForDeletes);
$this->notifyDeleteAuditLogs($queueForDeletes);
$this->notifyDeleteUsageStats($usageStatsRetentionHourly, $queueForDeletes);
$this->notifyDeleteExpiredSessions($queueForDeletes);
}
protected function foreachProject(Database $dbForConsole, callable $callback): void
protected function foreachProject(Database $dbForPlatform, callable $callback): void
{
// TODO: @Meldiron name of this method no longer matches. It does not delete, and it gives whole document
$count = 0;
@@ -82,7 +81,7 @@ class Maintenance extends Action
$executionStart = \microtime(true);
while ($sum === $limit) {
$projects = $dbForConsole->find('projects', [Query::limit($limit), Query::offset($chunk * $limit)]);
$projects = $dbForPlatform->find('projects', [Query::limit($limit), Query::offset($chunk * $limit)]);
$chunk++;
@@ -106,13 +105,6 @@ class Maintenance extends Action
->trigger();
}
private function notifyDeleteAbuseLogs(Delete $queueForDeletes): void
{
$queueForDeletes
->setType(DELETE_TYPE_ABUSE)
->trigger();
}
private function notifyDeleteAuditLogs(Delete $queueForDeletes): void
{
$queueForDeletes
@@ -143,12 +135,13 @@ class Maintenance extends Action
->trigger();
}
private function renewCertificates(Database $dbForConsole, Certificate $queueForCertificate): void
private function renewCertificates(Database $dbForPlatform, Certificate $queueForCertificate): void
{
$time = DateTime::now();
$certificates = $dbForConsole->find('certificates', [
$certificates = $dbForPlatform->find('certificates', [
Query::lessThan('attempts', 5), // Maximum 5 attempts
Query::isNotNull('renewDate'),
Query::lessThanEqual('renewDate', $time), // includes 60 days cooldown (we have 30 days to renew)
Query::limit(200), // Limit 200 comes from LetsEncrypt (300 orders per 3 hours, keeping some for new domains)
]);
+10 -10
View File
@@ -30,12 +30,12 @@ class Migrate extends Action
->desc('Migrate Appwrite to new version')
/** @TODO APP_VERSION_STABLE needs to be defined */
->param('version', APP_VERSION_STABLE, new Text(8), 'Version to migrate to.', true)
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('register')
->callback(function ($version, $dbForConsole, $getProjectDB, Registry $register) {
\Co\run(function () use ($version, $dbForConsole, $getProjectDB, $register) {
$this->action($version, $dbForConsole, $getProjectDB, $register);
->callback(function ($version, $dbForPlatform, $getProjectDB, Registry $register) {
\Co\run(function () use ($version, $dbForPlatform, $getProjectDB, $register) {
$this->action($version, $dbForPlatform, $getProjectDB, $register);
});
});
}
@@ -58,7 +58,7 @@ class Migrate extends Action
}
}
public function action(string $version, Database $dbForConsole, callable $getProjectDB, Registry $register)
public function action(string $version, Database $dbForPlatform, callable $getProjectDB, Registry $register)
{
Authorization::disable();
if (!array_key_exists($version, Migration::$versions)) {
@@ -93,10 +93,10 @@ class Migrate extends Action
$count = 0;
try {
$totalProjects = $dbForConsole->count('projects') + 1;
$totalProjects = $dbForPlatform->count('projects') + 1;
} catch (\Throwable $th) {
$dbForConsole->setNamespace('_console');
$totalProjects = $dbForConsole->count('projects') + 1;
$dbForPlatform->setNamespace('_console');
$totalProjects = $dbForPlatform->count('projects') + 1;
}
$class = 'Appwrite\\Migration\\Version\\' . Migration::$versions[$version];
@@ -120,7 +120,7 @@ class Migrate extends Action
$projectDB = $getProjectDB($project);
$projectDB->disableValidation();
$migration
->setProject($project, $projectDB, $dbForConsole)
->setProject($project, $projectDB, $dbForPlatform)
->setPDO($register->get('db', true))
->execute();
} catch (\Throwable $th) {
@@ -132,7 +132,7 @@ class Migrate extends Action
}
$sum = \count($projects);
$projects = $dbForConsole->find('projects', [Query::limit($limit), Query::offset($offset)]);
$projects = $dbForPlatform->find('projects', [Query::limit($limit), Query::offset($offset)]);
$offset = $offset + $limit;
$count = $count + $sum;
+24 -9
View File
@@ -25,6 +25,9 @@ use Appwrite\Spec\Swagger2;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Platform\Action;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
class SDKs extends Action
{
@@ -37,23 +40,35 @@ class SDKs extends Action
{
$this
->desc('Generate Appwrite SDKs')
->callback(fn () => $this->action());
->param('platform', null, new Nullable(new Text(256)), 'Selected Platform', optional: true)
->param('sdk', null, new Nullable(new Text(256)), 'Selected SDK', optional:true)
->param('version', null, new Nullable(new Text(256)), 'Selected SDK', optional:true)
->param('git', null, new Nullable(new WhiteList(['yes', 'no'])), 'Should we use git push?', optional: true)
->param('production', null, new Nullable(new WhiteList(['yes', 'no'])), 'Should we push to production?', optional:true)
->param('message', null, new Nullable(new Text(256)), 'Commit Message', optional:true)
->callback([$this, 'action']);
}
public function action(): void
public function action(?string $selectedPlatform, ?string $selectedSDK, ?string $version, ?string $git, ?string $production, ?string $message)
{
$platforms = Config::getParam('platforms');
$selectedPlatform = Console::confirm('Choose Platform ("' . APP_PLATFORM_CLIENT . '", "' . APP_PLATFORM_SERVER . '", "' . APP_PLATFORM_CONSOLE . '" or "*" for all):');
$selectedSDK = \strtolower(Console::confirm('Choose SDK ("*" for all):'));
$version = Console::confirm('Choose an Appwrite version');
$git = (Console::confirm('Should we use git push? (yes/no)') == 'yes');
$production = ($git) ? (Console::confirm('Type "Appwrite" to push code to production git repos') == 'Appwrite') : false;
$message = ($git) ? Console::confirm('Please enter your commit message:') : '';
$selectedPlatform ??= Console::confirm('Choose Platform ("' . APP_PLATFORM_CLIENT . '", "' . APP_PLATFORM_SERVER . '", "' . APP_PLATFORM_CONSOLE . '" or "*" for all):');
$selectedSDK ??= \strtolower(Console::confirm('Choose SDK ("*" for all):'));
$version ??= Console::confirm('Choose an Appwrite version');
$git ??= Console::confirm('Should we use git push? (yes/no)');
$git = $git === 'yes';
if ($git) {
$production ??= Console::confirm('Type "Appwrite" to push code to production git repos');
$production = $production === 'Appwrite';
$message ??= Console::confirm('Please enter your commit message:');
}
if (!in_array($version, ['0.6.x', '0.7.x', '0.8.x', '0.9.x', '0.10.x', '0.11.x', '0.12.x', '0.13.x', '0.14.x', '0.15.x', '1.0.x', '1.1.x', '1.2.x', '1.3.x', '1.4.x', '1.5.x', '1.6.x', 'latest'])) {
throw new \Exception('Unknown version given');
}
$platforms = Config::getParam('platforms');
foreach ($platforms as $key => $platform) {
if ($selectedPlatform !== $key && $selectedPlatform !== '*') {
continue;
+24 -12
View File
@@ -9,6 +9,7 @@ use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Pools\Group;
use Utopia\System\System;
@@ -25,7 +26,7 @@ abstract class ScheduleBase extends Action
abstract public static function getName(): string;
abstract public static function getSupportedResource(): string;
abstract public static function getCollectionId(): string;
abstract protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void;
abstract protected function enqueueResources(Group $pools, Database $dbForPlatform, callable $getProjectDB): void;
public function __construct()
{
@@ -34,9 +35,20 @@ abstract class ScheduleBase extends Action
$this
->desc("Execute {$type}s scheduled in Appwrite")
->inject('pools')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->callback(fn (Group $pools, Database $dbForConsole, callable $getProjectDB) => $this->action($pools, $dbForConsole, $getProjectDB));
->callback(fn (Group $pools, Database $dbForPlatform, callable $getProjectDB) => $this->action($pools, $dbForPlatform, $getProjectDB));
}
protected function updateProjectAccess(Document $project, Database $dbForPlatform): void
{
if (!$project->isEmpty() && $project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$project->setAttribute('accessedAt', DateTime::now());
Authorization::skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
}
}
}
/**
@@ -44,7 +56,7 @@ abstract class ScheduleBase extends Action
* 2. Create timer that sync all changes from 'schedules' collection to local copy. Only reading changes thanks to 'resourceUpdatedAt' attribute
* 3. Create timer that prepares coroutines for soon-to-execute schedules. When it's ready, coroutine sleeps until exact time before sending request to worker.
*/
public function action(Group $pools, Database $dbForConsole, callable $getProjectDB): void
public function action(Group $pools, Database $dbForPlatform, callable $getProjectDB): void
{
Console::title(\ucfirst(static::getSupportedResource()) . ' scheduler V1');
Console::success(APP_NAME . ' ' . \ucfirst(static::getSupportedResource()) . ' scheduler v1 has started');
@@ -56,8 +68,8 @@ abstract class ScheduleBase extends Action
* @throws Exception
* @var Document $schedule
*/
$getSchedule = function (Document $schedule) use ($dbForConsole, $getProjectDB): array {
$project = $dbForConsole->getDocument('projects', $schedule->getAttribute('projectId'));
$getSchedule = function (Document $schedule) use ($dbForPlatform, $getProjectDB): array {
$project = $dbForPlatform->getDocument('projects', $schedule->getAttribute('projectId'));
$resource = $getProjectDB($project)->getDocument(
static::getCollectionId(),
@@ -91,7 +103,7 @@ abstract class ScheduleBase extends Action
$paginationQueries[] = Query::cursorAfter($latestDocument);
}
$results = $dbForConsole->find('schedules', \array_merge($paginationQueries, [
$results = $dbForPlatform->find('schedules', \array_merge($paginationQueries, [
Query::equal('region', [System::getEnv('_APP_REGION', 'default')]),
Query::equal('resourceType', [static::getSupportedResource()]),
Query::equal('active', [true]),
@@ -119,11 +131,11 @@ abstract class ScheduleBase extends Action
Console::success("Starting timers at " . DateTime::now());
run(function () use ($dbForConsole, &$lastSyncUpdate, $getSchedule, $pools, $getProjectDB) {
run(function () use ($dbForPlatform, &$lastSyncUpdate, $getSchedule, $pools, $getProjectDB) {
/**
* The timer synchronize $schedules copy with database collection.
*/
Timer::tick(static::UPDATE_TIMER * 1000, function () use ($dbForConsole, &$lastSyncUpdate, $getSchedule, $pools) {
Timer::tick(static::UPDATE_TIMER * 1000, function () use ($dbForPlatform, &$lastSyncUpdate, $getSchedule, $pools) {
$time = DateTime::now();
$timerStart = \microtime(true);
@@ -141,7 +153,7 @@ abstract class ScheduleBase extends Action
$paginationQueries[] = Query::cursorAfter($latestDocument);
}
$results = $dbForConsole->find('schedules', \array_merge($paginationQueries, [
$results = $dbForPlatform->find('schedules', \array_merge($paginationQueries, [
Query::equal('region', [System::getEnv('_APP_REGION', 'default')]),
Query::equal('resourceType', [static::getSupportedResource()]),
Query::greaterThanEqual('resourceUpdatedAt', $lastSyncUpdate),
@@ -179,10 +191,10 @@ abstract class ScheduleBase extends Action
Timer::tick(
static::ENQUEUE_TIMER * 1000,
fn () => $this->enqueueResources($pools, $dbForConsole, $getProjectDB)
fn () => $this->enqueueResources($pools, $dbForPlatform, $getProjectDB)
);
$this->enqueueResources($pools, $dbForConsole, $getProjectDB);
$this->enqueueResources($pools, $dbForPlatform, $getProjectDB);
});
}
}
@@ -27,7 +27,7 @@ class ScheduleExecutions extends ScheduleBase
return 'executions';
}
protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void
protected function enqueueResources(Group $pools, Database $dbForPlatform, callable $getProjectDB): void
{
$queue = $pools->get('queue')->pop();
$connection = $queue->getResource();
@@ -36,7 +36,7 @@ class ScheduleExecutions extends ScheduleBase
foreach ($this->schedules as $schedule) {
if (!$schedule['active']) {
$dbForConsole->deleteDocument(
$dbForPlatform->deleteDocument(
'schedules',
$schedule['$id'],
);
@@ -50,13 +50,15 @@ class ScheduleExecutions extends ScheduleBase
continue;
}
$data = $dbForConsole->getDocument(
$data = $dbForPlatform->getDocument(
'schedules',
$schedule['$id'],
)->getAttribute('data', []);
$delay = $scheduledAt->getTimestamp() - (new \DateTime())->getTimestamp();
$this->updateProjectAccess($schedule['project'], $dbForPlatform);
\go(function () use ($queueForFunctions, $schedule, $delay, $data) {
Co::sleep($delay);
@@ -74,7 +76,7 @@ class ScheduleExecutions extends ScheduleBase
->trigger();
});
$dbForConsole->deleteDocument(
$dbForPlatform->deleteDocument(
'schedules',
$schedule['$id'],
);
@@ -31,7 +31,7 @@ class ScheduleFunctions extends ScheduleBase
return 'functions';
}
protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void
protected function enqueueResources(Group $pools, Database $dbForPlatform, callable $getProjectDB): void
{
$timerStart = \microtime(true);
$time = DateTime::now();
@@ -70,7 +70,7 @@ class ScheduleFunctions extends ScheduleBase
}
foreach ($delayedExecutions as $delay => $scheduleKeys) {
\go(function () use ($delay, $scheduleKeys, $pools) {
\go(function () use ($delay, $scheduleKeys, $pools, $dbForPlatform) {
\sleep($delay); // in seconds
$queue = $pools->get('queue')->pop();
@@ -84,6 +84,8 @@ class ScheduleFunctions extends ScheduleBase
$schedule = $this->schedules[$scheduleKey];
$this->updateProjectAccess($schedule['project'], $dbForPlatform);
$queueForFunctions = new Func($connection);
$queueForFunctions
@@ -26,7 +26,7 @@ class ScheduleMessages extends ScheduleBase
return 'messages';
}
protected function enqueueResources(Group $pools, Database $dbForConsole, callable $getProjectDB): void
protected function enqueueResources(Group $pools, Database $dbForPlatform, callable $getProjectDB): void
{
foreach ($this->schedules as $schedule) {
if (!$schedule['active']) {
@@ -40,18 +40,20 @@ class ScheduleMessages extends ScheduleBase
continue;
}
\go(function () use ($schedule, $pools, $dbForConsole) {
\go(function () use ($schedule, $pools, $dbForPlatform) {
$queue = $pools->get('queue')->pop();
$connection = $queue->getResource();
$queueForMessaging = new Messaging($connection);
$this->updateProjectAccess($schedule['project'], $dbForPlatform);
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($schedule['resourceId'])
->setProject($schedule['project'])
->trigger();
$dbForConsole->deleteDocument(
$dbForPlatform->deleteDocument(
'schedules',
$schedule['$id'],
);
+1 -1
View File
@@ -61,7 +61,7 @@ class Specs extends Action
// Mock dependencies
App::setResource('request', fn () => $this->getRequest());
App::setResource('response', fn () => $response);
App::setResource('dbForConsole', fn () => new Database(new MySQL(''), new Cache(new None())));
App::setResource('dbForPlatform', fn () => new Database(new MySQL(''), new Cache(new None())));
App::setResource('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None())));
$platforms = [
+23 -20
View File
@@ -46,7 +46,7 @@ class Builds extends Action
$this
->desc('Builds worker')
->inject('message')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForEvents')
->inject('queueForFunctions')
->inject('queueForUsage')
@@ -54,12 +54,12 @@ class Builds extends Action
->inject('dbForProject')
->inject('deviceForFunctions')
->inject('log')
->callback(fn ($message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log) => $this->action($message, $dbForConsole, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $log));
->callback(fn ($message, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log) => $this->action($message, $dbForPlatform, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $log));
}
/**
* @param Message $message
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Event $queueForEvents
* @param Func $queueForFunctions
* @param Usage $queueForUsage
@@ -70,7 +70,7 @@ class Builds extends Action
* @return void
* @throws \Utopia\Database\Exception
*/
public function action(Message $message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log): void
public function action(Message $message, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log): void
{
$payload = $message->getPayload() ?? [];
@@ -92,7 +92,7 @@ class Builds extends Action
case BUILD_TYPE_RETRY:
Console::info('Creating build for deployment: ' . $deployment->getId());
$github = new GitHub($cache);
$this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForConsole, $dbForProject, $github, $project, $resource, $deployment, $template, $log);
$this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForPlatform, $dbForProject, $github, $project, $resource, $deployment, $template, $log);
break;
default:
@@ -105,7 +105,7 @@ class Builds extends Action
* @param Func $queueForFunctions
* @param Event $queueForEvents
* @param Usage $queueForUsage
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Database $dbForProject
* @param GitHub $github
* @param Document $project
@@ -117,7 +117,7 @@ class Builds extends Action
* @throws \Utopia\Database\Exception
* @throws Exception
*/
protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForConsole, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log): void
protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForPlatform, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log): void
{
$executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
@@ -199,7 +199,7 @@ class Builds extends Action
$repositoryName = '';
if ($isVcsEnabled) {
$installation = $dbForConsole->getDocument('installations', $installationId);
$installation = $dbForPlatform->getDocument('installations', $installationId);
$providerInstallationId = $installation->getAttribute('providerInstallationId');
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
@@ -209,8 +209,7 @@ class Builds extends Action
try {
if ($isNewBuild && !$isVcsEnabled) {
// Non-vcs+Template
// Non-VCS + Template
$templateRepositoryName = $template->getAttribute('repositoryName', '');
$templateOwnerName = $template->getAttribute('ownerName', '');
$templateVersion = $template->getAttribute('version', '');
@@ -233,6 +232,8 @@ class Builds extends Action
throw new \Exception('Unable to clone code repository: ' . $stderr);
}
Console::execute('find ' . \escapeshellarg($tmpTemplateDirectory) . ' -type d -name ".git" -exec rm -rf {} +', '', $stdout, $stderr);
// Ensure directories
Console::execute('mkdir -p ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory), '', $stdout, $stderr);
@@ -398,6 +399,8 @@ class Builds extends Action
throw new \Exception('Repository directory size should be less than ' . number_format($functionsSizeLimit / 1048576, 2) . ' MBs.');
}
Console::execute('find ' . \escapeshellarg($tmpDirectory) . ' -type d -name ".git" -exec rm -rf {} +', '', $stdout, $stderr);
$tarParamDirectory = '/tmp/builds/' . $buildId . '/code' . (empty($rootDirectory) ? '' : '/' . $rootDirectory);
Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $stdout, $stderr); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax
@@ -415,7 +418,7 @@ class Builds extends Action
$directorySize = $deviceForFunctions->getFileSize($source);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttribute('path', $source)->setAttribute('size', $directorySize));
$this->runGitAction('processing', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
$this->runGitAction('processing', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForPlatform);
}
/** Request the executor to build the code... */
@@ -423,7 +426,7 @@ class Builds extends Action
$build = $dbForProject->updateDocument('builds', $buildId, $build);
if ($isVcsEnabled) {
$this->runGitAction('building', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
$this->runGitAction('building', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForPlatform);
}
/** Trigger Webhook */
@@ -637,7 +640,7 @@ class Builds extends Action
$build = $dbForProject->updateDocument('builds', $buildId, $build);
if ($isVcsEnabled) {
$this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
$this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForPlatform);
}
Console::success("Build id: $buildId created");
@@ -658,12 +661,12 @@ class Builds extends Action
/** Update function schedule */
// Inform scheduler if function is still active
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
} catch (\Throwable $th) {
if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
@@ -680,7 +683,7 @@ class Builds extends Action
$build = $dbForProject->updateDocument('builds', $buildId, $build);
if ($isVcsEnabled) {
$this->runGitAction('failed', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
$this->runGitAction('failed', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForPlatform);
}
} finally {
/**
@@ -739,7 +742,7 @@ class Builds extends Action
* @param Document $function
* @param string $deploymentId
* @param Database $dbForProject
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @return void
* @throws Structure
* @throws \Utopia\Database\Exception
@@ -747,7 +750,7 @@ class Builds extends Action
* @throws Conflict
* @throws Restricted
*/
protected function runGitAction(string $status, GitHub $github, string $providerCommitHash, string $owner, string $repositoryName, Document $project, Document $function, string $deploymentId, Database $dbForProject, Database $dbForConsole): void
protected function runGitAction(string $status, GitHub $github, string $providerCommitHash, string $owner, string $repositoryName, Document $project, Document $function, string $deploymentId, Database $dbForProject, Database $dbForPlatform): void
{
if ($function->getAttribute('providerSilentMode', false) === true) {
return;
@@ -792,7 +795,7 @@ class Builds extends Action
$retries++;
try {
$dbForConsole->createDocument('vcsCommentLocks', new Document([
$dbForPlatform->createDocument('vcsCommentLocks', new Document([
'$id' => $commentId
]));
break;
@@ -812,7 +815,7 @@ class Builds extends Action
$comment->addBuild($project, $function, $status, $deployment->getId(), ['type' => 'logs']);
$github->updateComment($owner, $repositoryName, $commentId, $comment->generateComment());
} finally {
$dbForConsole->deleteDocument('vcsCommentLocks', $commentId);
$dbForPlatform->deleteDocument('vcsCommentLocks', $commentId);
}
}
}
+57 -197
View File
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Certificates\Adapter as CertificatesAdapter;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
@@ -11,7 +12,6 @@ use Appwrite\Template\Template;
use Appwrite\Utopia\Response\Model\Rule;
use Exception;
use Throwable;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@@ -44,28 +44,31 @@ class Certificates extends Action
$this
->desc('Certificates worker')
->inject('message')
->inject('project')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForMails')
->inject('queueForEvents')
->inject('queueForFunctions')
->inject('log')
->callback(fn (Message $message, Document $project, Database $dbForConsole, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, Log $log) => $this->action($message, $project, $dbForConsole, $queueForMails, $queueForEvents, $queueForFunctions, $log));
->inject('certificates')
->callback(
fn (Message $message, Database $dbForPlatform, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, Log $log, CertificatesAdapter $certificates) =>
$this->action($message, $dbForPlatform, $queueForMails, $queueForEvents, $queueForFunctions, $log, $certificates)
);
}
/**
* @param Message $message
* @param Document $project
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Mail $queueForMails
* @param Event $queueForEvents
* @param Func $queueForFunctions
* @param Log $log
* @param CertificatesAdapter $certificates
* @return void
* @throws Throwable
* @throws \Utopia\Database\Exception
*/
public function action(Message $message, Document $project, Database $dbForConsole, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, Log $log): void
public function action(Message $message, Database $dbForPlatform, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, Log $log, CertificatesAdapter $certificates): void
{
$payload = $message->getPayload() ?? [];
@@ -79,33 +82,33 @@ class Certificates extends Action
$log->addTag('domain', $domain->get());
$this->execute($domain, $project, $dbForConsole, $queueForMails, $queueForEvents, $queueForFunctions, $log, $skipRenewCheck);
$this->execute($domain, $dbForPlatform, $queueForMails, $queueForEvents, $queueForFunctions, $log, $certificates, $skipRenewCheck);
}
/**
* @param Domain $domain
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Mail $queueForMails
* @param Event $queueForEvents
* @param Func $queueForFunctions
* @param CertificatesAdapter $certificates
* @param bool $skipRenewCheck
* @return void
* @throws Throwable
* @throws \Utopia\Database\Exception
*/
private function execute(Domain $domain, Document $project, Database $dbForConsole, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, Log $log, bool $skipRenewCheck = false): void
private function execute(Domain $domain, Database $dbForPlatform, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, Log $log, CertificatesAdapter $certificates, bool $skipRenewCheck = false): void
{
/**
* 1. Read arguments and validate domain
* 2. Get main domain
* 3. Validate CNAME DNS if parameter is not main domain (meaning it's custom domain)
* 4. Validate security email. Cannot be empty, required by LetsEncrypt
* 5. Validate renew date with certificate file, unless requested to skip by parameter
* 6. Issue a certificate using certbot CLI
* 7. Update 'log' attribute on certificate document with Certbot message
* 8. Create storage folder for certificate, if not ready already
* 9. Move certificates from Certbot location to our Storage
* 10. Create/Update our Storage with new Traefik config with new certificate paths
* 4. Validate renew date with certificate file, unless requested to skip by parameter
* 5. Issue a certificate using certbot CLI
* 6. Update 'log' attribute on certificate document with Certbot message
* 7. Create storage folder for certificate, if not ready already
* 8. Move certificates from Certbot location to our Storage
* 9. Create/Update our Storage with new Traefik config with new certificate paths
* 11. Read certificate file and update 'renewDate' on certificate document
* 12. Update 'issueDate' and 'attempts' on certificate
*
@@ -122,11 +125,11 @@ class Certificates extends Action
* 2. Save document to database
* 3. Update all domains documents with current certificate ID
*
* Note: Renewals are checked and scheduled from maintenence worker
* Note: Renewals are checked and scheduled from maintenance worker
*/
// Get current certificate
$certificate = $dbForConsole->findOne('certificates', [Query::equal('domain', [$domain->get()])]);
$certificate = $dbForPlatform->findOne('certificates', [Query::equal('domain', [$domain->get()])]);
// If we don't have certificate for domain yet, let's create new document. At the end we save it
if ($certificate->isEmpty()) {
@@ -137,50 +140,27 @@ class Certificates extends Action
$success = false;
try {
// Email for alerts is required by LetsEncrypt
$email = System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'));
if (empty($email)) {
throw new Exception('You must set a valid security email address (_APP_EMAIL_CERTIFICATES) to issue an SSL certificate.');
}
// Validate domain and DNS records. Skip if job is forced
if (!$skipRenewCheck) {
$mainDomain = $this->getMainDomain();
$isMainDomain = !isset($mainDomain) || $domain->get() === $mainDomain;
$this->validateDomain($domain, $isMainDomain, $log);
// If certificate exists already, double-check expiry date. Skip if job is forced
if (!$certificates->isRenewRequired($domain->get(), $log)) {
throw new Exception('Renew isn\'t required.');
}
}
// If certificate exists already, double-check expiry date. Skip if job is forced
if (!$skipRenewCheck && !$this->isRenewRequired($domain->get(), $log)) {
throw new Exception('Renew isn\'t required.');
}
// Prepare folder name for certbot. Using this helps prevent miss-match in LetsEncrypt configuration when renewing certificate
$folder = ID::unique();
try {
// Generate certificate files using Let's Encrypt
$letsEncryptData = $this->issueCertificate($folder, $domain->get(), $email);
// Give certificates to Traefik
$this->applyCertificateFiles($folder, $domain->get(), $letsEncryptData);
} catch (\Throwable $th) {
Console::error('Failed to generate Lets Encrypt certificate');
}
// Prepare unique cert name. Using this helps prevent miss-match in configuration when renewing certificates.
$certName = ID::unique();
$renewDate = $certificates->issueCertificate($certName, $domain->get());
// Command succeeded, store all data into document
$logs = 'Certificate successfully generated.';
$certificate->setAttribute('logs', \mb_strcut($logs, 0, 1000000));// Limit to 1MB
try {
// TEMP: add custom hostnames to cloudflare
$this->addCustomHostnameToRegistrar($project, $domain->get());
} catch (\Throwable $th) {
Console::error('Failed to add custom hostname to registrar: ' . $th->getMessage());
}
$certificate->setAttribute('logs', 'Certificate successfully generated.');
// Update certificate info stored in database
$certificate->setAttribute('renewDate', $this->getRenewDate($domain->get()));
$certificate->setAttribute('renewDate', $renewDate);
$certificate->setAttribute('attempts', 0);
$certificate->setAttribute('issueDate', DateTime::now());
$success = true;
@@ -194,7 +174,7 @@ class Certificates extends Action
$attempts = $certificate->getAttribute('attempts', 0) + 1;
$certificate->setAttribute('attempts', $attempts);
// Store cuttent time as renew date to ensure another attempt in next maintenance cycle
// Store current time as renew date to ensure another attempt in next maintenance cycle.
$certificate->setAttribute('renewDate', DateTime::now());
// Send email to security email
@@ -206,7 +186,7 @@ class Certificates extends Action
$certificate->setAttribute('updated', DateTime::now());
// Save all changes we made to certificate document into database
$this->saveCertificateDocument($domain->get(), $certificate, $success, $dbForConsole, $queueForEvents, $queueForFunctions);
$this->saveCertificateDocument($domain->get(), $certificate, $success, $dbForPlatform, $queueForEvents, $queueForFunctions);
}
}
@@ -245,7 +225,7 @@ class Certificates extends Action
* @param string $domain Domain name that certificate is for
* @param Document $certificate Certificate document that we need to save
* @param bool $success
* @param Database $dbForConsole Database connection for console
* @param Database $dbForPlatform Database connection for console
* @param Event $queueForEvents
* @param Func $queueForFunctions
* @return void
@@ -254,21 +234,21 @@ class Certificates extends Action
* @throws Conflict
* @throws Structure
*/
private function saveCertificateDocument(string $domain, Document $certificate, bool $success, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions): void
private function saveCertificateDocument(string $domain, Document $certificate, bool $success, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions): void
{
// Check if update or insert required
$certificateDocument = $dbForConsole->findOne('certificates', [Query::equal('domain', [$domain])]);
$certificateDocument = $dbForPlatform->findOne('certificates', [Query::equal('domain', [$domain])]);
if (!$certificateDocument->isEmpty()) {
// Merge new data with current data
$certificate = new Document(\array_merge($certificateDocument->getArrayCopy(), $certificate->getArrayCopy()));
$certificate = $dbForConsole->updateDocument('certificates', $certificate->getId(), $certificate);
$certificate = $dbForPlatform->updateDocument('certificates', $certificate->getId(), $certificate);
} else {
$certificate->removeAttribute('$internalId');
$certificate = $dbForConsole->createDocument('certificates', $certificate);
$certificate = $dbForPlatform->createDocument('certificates', $certificate);
}
$certificateId = $certificate->getId();
$this->updateDomainDocuments($certificateId, $domain, $success, $dbForConsole, $queueForEvents, $queueForFunctions);
$this->updateDomainDocuments($certificateId, $domain, $success, $dbForPlatform, $queueForEvents, $queueForFunctions);
}
/**
@@ -287,8 +267,8 @@ class Certificates extends Action
}
/**
* Internal domain validation functionality to prevent unnecessary attempts failed from Let's Encrypt side. We check:
* - Domain needs to be public and valid (prevents NFT domains that are not supported by Let's Encrypt)
* Internal domain validation functionality to prevent unnecessary attempts. We check:
* - Domain needs to be public and valid (prevents NFT domains that are not supported)
* - Domain must have proper DNS record
*
* @param Domain $domain Domain which we validate
@@ -334,136 +314,6 @@ class Certificates extends Action
}
}
/**
* Reads expiry date of certificate from file and decides if renewal is required or not.
*
* @param string $domain Domain for which we check certificate file
* @return bool True, if certificate needs to be renewed
* @throws Exception
*/
private function isRenewRequired(string $domain, Log $log): bool
{
$certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem';
if (\file_exists($certPath)) {
$validTo = null;
$certData = openssl_x509_parse(file_get_contents($certPath));
$validTo = $certData['validTo_time_t'] ?? 0;
if (empty($validTo)) {
$log->addTag('certificateDomain', $domain);
throw new Exception('Unable to read certificate file (cert.pem).');
}
// LetsEncrypt allows renewal 30 days before expiry
$expiryInAdvance = (60 * 60 * 24 * 30);
if ($validTo - $expiryInAdvance > \time()) {
$log->addTag('certificateDomain', $domain);
$log->addExtra('certificateData', \is_array($certData) ? \json_encode($certData) : \strval($certData));
return false;
}
}
return true;
}
/**
* LetsEncrypt communication to issue certificate (using certbot CLI)
*
* @param string $folder Folder into which certificates should be generated
* @param string $domain Domain to generate certificate for
* @return array Named array with keys 'stdout' and 'stderr', both string
* @throws Exception
*/
private function issueCertificate(string $folder, string $domain, string $email): array
{
$stdout = '';
$stderr = '';
$staging = (App::isProduction()) ? '' : ' --dry-run';
$exit = Console::execute("certbot certonly -v --webroot --noninteractive --agree-tos{$staging}"
. " --email " . $email
. " --cert-name " . $folder
. " -w " . APP_STORAGE_CERTIFICATES
. " -d {$domain}", '', $stdout, $stderr);
// Unexpected error, usually 5XX, API limits, ...
if ($exit !== 0) {
throw new Exception('Failed to issue a certificate with message: ' . $stderr);
}
return [
'stdout' => $stdout,
'stderr' => $stderr
];
}
/**
* Read new renew date from certificate file generated by Let's Encrypt
*
* @param string $domain Domain which certificate was generated for
* @return string
* @throws \Utopia\Database\Exception
*/
private function getRenewDate(string $domain): string
{
$certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem';
$certData = openssl_x509_parse(file_get_contents($certPath));
$validTo = $certData['validTo_time_t'] ?? null;
$dt = (new \DateTime())->setTimestamp($validTo);
return DateTime::addSeconds($dt, -60 * 60 * 24 * 30); // -30 days
}
/**
* Method to take files from Let's Encrypt, and put it into Traefik.
*
* @param string $domain Domain which certificate was generated for
* @param string $folder Folder in which certificates were generated
* @param array $letsEncryptData Let's Encrypt logs to use for additional info when throwing error
* @return void
* @throws Exception
*/
private function applyCertificateFiles(string $folder, string $domain, array $letsEncryptData): void
{
// Prepare folder in storage for domain
$path = APP_STORAGE_CERTIFICATES . '/' . $domain;
if (!\is_readable($path)) {
if (!\mkdir($path, 0755, true)) {
throw new Exception('Failed to create path for certificate.');
}
}
// Move generated files
if (!@\rename('/etc/letsencrypt/live/' . $folder . '/cert.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem')) {
throw new Exception('Failed to rename certificate cert.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']);
}
if (!@\rename('/etc/letsencrypt/live/' . $folder . '/chain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/chain.pem')) {
throw new Exception('Failed to rename certificate chain.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']);
}
if (!@\rename('/etc/letsencrypt/live/' . $folder . '/fullchain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/fullchain.pem')) {
throw new Exception('Failed to rename certificate fullchain.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']);
}
if (!@\rename('/etc/letsencrypt/live/' . $folder . '/privkey.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/privkey.pem')) {
throw new Exception('Failed to rename certificate privkey.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']);
}
$config = \implode(PHP_EOL, [
"tls:",
" certificates:",
" - certFile: /storage/certificates/{$domain}/fullchain.pem",
" keyFile: /storage/certificates/{$domain}/privkey.pem"
]);
// Save configuration into Traefik using our new cert files
if (!\file_put_contents(APP_STORAGE_CONFIG . '/' . $domain . '.yml', $config)) {
throw new Exception('Failed to save Traefik configuration.');
}
}
/**
* Method to make sure information about error is delivered to admnistrator.
*
@@ -517,15 +367,21 @@ class Certificates extends Action
*
* @return void
*/
private function updateDomainDocuments(string $certificateId, string $domain, bool $success, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions): void
private function updateDomainDocuments(string $certificateId, string $domain, bool $success, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions): void
{
$rule = $dbForConsole->getDocument('rules', md5($domain));
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$rule = $dbForPlatform->getDocument('rules', md5($domain));
} else {
$rule = $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain]),
]);
}
if (!$rule->isEmpty()) {
$rule->setAttribute('certificateId', $certificateId);
$rule->setAttribute('status', $success ? 'verified' : 'unverified');
$dbForConsole->updateDocument('rules', $rule->getId(), $rule);
$dbForPlatform->updateDocument('rules', $rule->getId(), $rule);
$projectId = $rule->getAttribute('projectId');
@@ -534,7 +390,11 @@ class Certificates extends Action
return;
}
$project = $dbForConsole->getDocument('projects', $projectId);
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
return;
}
/** Trigger Webhook */
$ruleModel = new Rule();
+47 -38
View File
@@ -11,6 +11,7 @@ use Utopia\Database\Document;
use Utopia\Database\Exception as DatabaseException;
use Utopia\Database\Exception\Authorization;
use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\NotFound;
use Utopia\Database\Exception\Restricted;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Query;
@@ -33,21 +34,21 @@ class Databases extends Action
$this
->desc('Databases worker')
->inject('message')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('dbForProject')
->inject('log')
->callback(fn (Message $message, Database $dbForConsole, Database $dbForProject, Log $log) => $this->action($message, $dbForConsole, $dbForProject, $log));
->callback(fn (Message $message, Database $dbForPlatform, Database $dbForProject, Log $log) => $this->action($message, $dbForPlatform, $dbForProject, $log));
}
/**
* @param Message $message
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Database $dbForProject
* @param Log $log
* @return void
* @throws \Exception
*/
public function action(Message $message, Database $dbForConsole, Database $dbForProject, Log $log): void
public function action(Message $message, Database $dbForPlatform, Database $dbForProject, Log $log): void
{
$payload = $message->getPayload() ?? [];
@@ -73,10 +74,10 @@ class Databases extends Action
match (\strval($type)) {
DATABASE_TYPE_DELETE_DATABASE => $this->deleteDatabase($database, $project, $dbForProject),
DATABASE_TYPE_DELETE_COLLECTION => $this->deleteCollection($database, $collection, $project, $dbForProject),
DATABASE_TYPE_CREATE_ATTRIBUTE => $this->createAttribute($database, $collection, $document, $project, $dbForConsole, $dbForProject),
DATABASE_TYPE_DELETE_ATTRIBUTE => $this->deleteAttribute($database, $collection, $document, $project, $dbForConsole, $dbForProject),
DATABASE_TYPE_CREATE_INDEX => $this->createIndex($database, $collection, $document, $project, $dbForConsole, $dbForProject),
DATABASE_TYPE_DELETE_INDEX => $this->deleteIndex($database, $collection, $document, $project, $dbForConsole, $dbForProject),
DATABASE_TYPE_CREATE_ATTRIBUTE => $this->createAttribute($database, $collection, $document, $project, $dbForPlatform, $dbForProject),
DATABASE_TYPE_DELETE_ATTRIBUTE => $this->deleteAttribute($database, $collection, $document, $project, $dbForPlatform, $dbForProject),
DATABASE_TYPE_CREATE_INDEX => $this->createIndex($database, $collection, $document, $project, $dbForPlatform, $dbForProject),
DATABASE_TYPE_DELETE_INDEX => $this->deleteIndex($database, $collection, $document, $project, $dbForPlatform, $dbForProject),
default => throw new \Exception('No database operation for type: ' . \strval($type)),
};
}
@@ -86,7 +87,7 @@ class Databases extends Action
* @param Document $collection
* @param Document $attribute
* @param Document $project
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Database $dbForProject
* @return void
* @throws Authorization
@@ -94,7 +95,7 @@ class Databases extends Action
* @throws \Exception
* @throws \Throwable
*/
private function createAttribute(Document $database, Document $collection, Document $attribute, Document $project, Database $dbForConsole, Database $dbForProject): void
private function createAttribute(Document $database, Document $collection, Document $attribute, Document $project, Database $dbForPlatform, Database $dbForProject): void
{
if ($collection->isEmpty()) {
throw new Exception('Missing collection');
@@ -133,7 +134,7 @@ class Databases extends Action
$formatOptions = $attribute->getAttribute('formatOptions', []);
$filters = $attribute->getAttribute('filters', []);
$options = $attribute->getAttribute('options', []);
$project = $dbForConsole->getDocument('projects', $projectId);
$project = $dbForPlatform->getDocument('projects', $projectId);
try {
switch ($type) {
@@ -210,7 +211,7 @@ class Databases extends Action
* @param Document $collection
* @param Document $attribute
* @param Document $project
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Database $dbForProject
* @return void
* @throws Authorization
@@ -218,7 +219,7 @@ class Databases extends Action
* @throws \Exception
* @throws \Throwable
**/
private function deleteAttribute(Document $database, Document $collection, Document $attribute, Document $project, Database $dbForConsole, Database $dbForProject): void
private function deleteAttribute(Document $database, Document $collection, Document $attribute, Document $project, Database $dbForPlatform, Database $dbForProject): void
{
if ($collection->isEmpty()) {
throw new Exception('Missing collection');
@@ -238,7 +239,7 @@ class Databases extends Action
$key = $attribute->getAttribute('key', '');
$status = $attribute->getAttribute('status', '');
$type = $attribute->getAttribute('type', '');
$project = $dbForConsole->getDocument('projects', $projectId);
$project = $dbForPlatform->getDocument('projects', $projectId);
$options = $attribute->getAttribute('options', []);
$relatedAttribute = new Document();
$relatedCollection = new Document();
@@ -251,23 +252,21 @@ class Databases extends Action
try {
try {
if ($status !== 'failed') {
if ($type === Database::VAR_RELATIONSHIP) {
if ($options['twoWay']) {
$relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $options['relatedCollection']);
if ($relatedCollection->isEmpty()) {
throw new DatabaseException('Collection not found');
}
$relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']);
if ($type === Database::VAR_RELATIONSHIP) {
if ($options['twoWay']) {
$relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $options['relatedCollection']);
if ($relatedCollection->isEmpty()) {
throw new DatabaseException('Collection not found');
}
if (!$dbForProject->deleteRelationship('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'stuck'));
throw new DatabaseException('Failed to delete Relationship');
}
} elseif (!$dbForProject->deleteAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
throw new DatabaseException('Failed to delete Attribute');
$relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']);
}
if (!$dbForProject->deleteRelationship('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'stuck'));
throw new DatabaseException('Failed to delete Relationship');
}
} elseif (!$dbForProject->deleteAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
throw new DatabaseException('Failed to delete Attribute');
}
$dbForProject->deleteDocument('attributes', $attribute->getId());
@@ -275,6 +274,18 @@ class Databases extends Action
if (!$relatedAttribute->isEmpty()) {
$dbForProject->deleteDocument('attributes', $relatedAttribute->getId());
}
} catch (NotFound $e) {
Console::error($e->getMessage());
$dbForProject->deleteDocument('attributes', $attribute->getId());
if (!$relatedAttribute->isEmpty()) {
$dbForProject->deleteDocument('attributes', $relatedAttribute->getId());
}
throw $e;
} catch (\Throwable $e) {
Console::error($e->getMessage());
@@ -345,7 +356,7 @@ class Databases extends Action
}
if ($exists) { // Delete the duplicate if created, else update in db
$this->deleteIndex($database, $collection, $index, $project, $dbForConsole, $dbForProject);
$this->deleteIndex($database, $collection, $index, $project, $dbForPlatform, $dbForProject);
} else {
$dbForProject->updateDocument('indexes', $index->getId(), $index);
}
@@ -354,11 +365,9 @@ class Databases extends Action
}
} finally {
$dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId);
$dbForProject->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId());
if (!$relatedCollection->isEmpty() && !$relatedAttribute->isEmpty()) {
$dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $relatedCollection->getId());
$dbForProject->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId());
}
}
}
@@ -368,7 +377,7 @@ class Databases extends Action
* @param Document $collection
* @param Document $index
* @param Document $project
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Database $dbForProject
* @return void
* @throws Authorization
@@ -377,7 +386,7 @@ class Databases extends Action
* @throws DatabaseException
* @throws \Throwable
*/
private function createIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForConsole, Database $dbForProject): void
private function createIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForPlatform, Database $dbForProject): void
{
if ($collection->isEmpty()) {
throw new Exception('Missing collection');
@@ -399,7 +408,7 @@ class Databases extends Action
$attributes = $index->getAttribute('attributes', []);
$lengths = $index->getAttribute('lengths', []);
$orders = $index->getAttribute('orders', []);
$project = $dbForConsole->getDocument('projects', $projectId);
$project = $dbForPlatform->getDocument('projects', $projectId);
try {
if (!$dbForProject->createIndex('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key, $type, $attributes, $lengths, $orders)) {
@@ -429,7 +438,7 @@ class Databases extends Action
* @param Document $collection
* @param Document $index
* @param Document $project
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Database $dbForProject
* @return void
* @throws Authorization
@@ -438,7 +447,7 @@ class Databases extends Action
* @throws DatabaseException
* @throws \Throwable
*/
private function deleteIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForConsole, Database $dbForProject): void
private function deleteIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForPlatform, Database $dbForProject): void
{
if ($collection->isEmpty()) {
throw new Exception('Missing collection');
@@ -456,7 +465,7 @@ class Databases extends Action
]);
$key = $index->getAttribute('key');
$status = $index->getAttribute('status', '');
$project = $dbForConsole->getDocument('projects', $projectId);
$project = $dbForPlatform->getDocument('projects', $projectId);
try {
if ($status !== 'failed' && !$dbForProject->deleteIndex('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
+87 -112
View File
@@ -3,11 +3,10 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Auth\Auth;
use Appwrite\Certificates\Adapter as CertificatesAdapter;
use Appwrite\Extend\Exception;
use Executor\Executor;
use Throwable;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\Database\TimeLimit;
use Utopia\Audit\Audit;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
@@ -44,24 +43,28 @@ class Deletes extends Action
$this
->desc('Deletes worker')
->inject('message')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('timelimit')
->inject('deviceForFiles')
->inject('deviceForFunctions')
->inject('deviceForBuilds')
->inject('deviceForCache')
->inject('abuseRetention')
->inject('certificates')
->inject('executionRetention')
->inject('auditRetention')
->inject('log')
->callback(fn ($message, $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log) => $this->action($message, $dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $abuseRetention, $executionRetention, $auditRetention, $log));
->callback(
fn ($message, $dbForPlatform, callable $getProjectDB, callable $timelimit, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, CertificatesAdapter $certificates, string $executionRetention, string $auditRetention, Log $log) =>
$this->action($message, $dbForPlatform, $getProjectDB, $timelimit, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $certificates, $executionRetention, $auditRetention, $log)
);
}
/**
* @throws Exception
* @throws Throwable
*/
public function action(Message $message, Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log): void
public function action(Message $message, Database $dbForPlatform, callable $getProjectDB, callable $timelimit, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, CertificatesAdapter $certificates, string $executionRetention, string $auditRetention, Log $log): void
{
$payload = $message->getPayload() ?? [];
@@ -84,10 +87,10 @@ class Deletes extends Action
case DELETE_TYPE_DOCUMENT:
switch ($document->getCollection()) {
case DELETE_TYPE_PROJECTS:
$this->deleteProject($dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $document);
$this->deleteProject($dbForPlatform, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $certificates, $document);
break;
case DELETE_TYPE_FUNCTIONS:
$this->deleteFunction($dbForConsole, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $document, $project);
$this->deleteFunction($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $certificates, $document, $project);
break;
case DELETE_TYPE_DEPLOYMENTS:
$this->deleteDeployment($getProjectDB, $deviceForFunctions, $deviceForBuilds, $document, $project);
@@ -99,10 +102,10 @@ class Deletes extends Action
$this->deleteBucket($getProjectDB, $deviceForFiles, $document, $project);
break;
case DELETE_TYPE_INSTALLATIONS:
$this->deleteInstallation($dbForConsole, $getProjectDB, $document, $project);
$this->deleteInstallation($dbForPlatform, $getProjectDB, $document, $project);
break;
case DELETE_TYPE_RULES:
$this->deleteRule($dbForConsole, $document);
$this->deleteRule($dbForPlatform, $document, $certificates);
break;
default:
Console::error('No lazy delete operation available for document of type: ' . $document->getCollection());
@@ -110,7 +113,7 @@ class Deletes extends Action
}
break;
case DELETE_TYPE_TEAM_PROJECTS:
$this->deleteProjectsByTeam($dbForConsole, $getProjectDB, $document);
$this->deleteProjectsByTeam($dbForPlatform, $getProjectDB, $certificates, $document);
break;
case DELETE_TYPE_EXECUTIONS:
$this->deleteExecutionLogs($project, $getProjectDB, $executionRetention);
@@ -120,11 +123,8 @@ class Deletes extends Action
$this->deleteAuditLogs($project, $getProjectDB, $auditRetention);
}
break;
case DELETE_TYPE_ABUSE:
$this->deleteAbuseLogs($project, $getProjectDB, $abuseRetention);
break;
case DELETE_TYPE_REALTIME:
$this->deleteRealtimeUsage($dbForConsole, $datetime);
$this->deleteRealtimeUsage($dbForPlatform, $datetime);
break;
case DELETE_TYPE_SESSIONS:
$this->deleteExpiredSessions($project, $getProjectDB);
@@ -139,7 +139,7 @@ class Deletes extends Action
$this->deleteCacheByDate($project, $getProjectDB, $datetime);
break;
case DELETE_TYPE_SCHEDULES:
$this->deleteSchedules($dbForConsole, $getProjectDB, $datetime);
$this->deleteSchedules($dbForPlatform, $getProjectDB, $datetime);
break;
case DELETE_TYPE_TOPIC:
$this->deleteTopic($project, $getProjectDB, $document);
@@ -159,7 +159,7 @@ class Deletes extends Action
}
/**
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param callable $getProjectDB
* @param string $datetime
* @param Document|null $document
@@ -170,7 +170,7 @@ class Deletes extends Action
* @throws Structure
* @throws DatabaseException
*/
private function deleteSchedules(Database $dbForConsole, callable $getProjectDB, string $datetime): void
private function deleteSchedules(Database $dbForPlatform, callable $getProjectDB, string $datetime): void
{
$this->listByGroup(
'schedules',
@@ -179,12 +179,12 @@ class Deletes extends Action
Query::lessThanEqual('resourceUpdatedAt', $datetime),
Query::equal('active', [false]),
],
$dbForConsole,
function (Document $document) use ($dbForConsole, $getProjectDB) {
$project = $dbForConsole->getDocument('projects', $document->getAttribute('projectId'));
$dbForPlatform,
function (Document $document) use ($dbForPlatform, $getProjectDB) {
$project = $dbForPlatform->getDocument('projects', $document->getAttribute('projectId'));
if ($project->isEmpty()) {
$dbForConsole->deleteDocument('schedules', $document->getId());
$dbForPlatform->deleteDocument('schedules', $document->getId());
Console::success('Deleted schedule for deleted project ' . $document->getAttribute('projectId'));
return;
}
@@ -208,7 +208,7 @@ class Deletes extends Action
}
if ($delete) {
$dbForConsole->deleteDocument('schedules', $document->getId());
$dbForPlatform->deleteDocument('schedules', $document->getId());
Console::success('Deleting schedule for ' . $document->getAttribute('resourceType') . ' ' . $document->getAttribute('resourceId'));
}
}
@@ -264,7 +264,7 @@ class Deletes extends Action
MESSAGE_TYPE_EMAIL => 'emailTotal',
MESSAGE_TYPE_SMS => 'smsTotal',
MESSAGE_TYPE_PUSH => 'pushTotal',
default => throw new Exception('Invalid target provider type'),
default => throw new Exception('Invalid target CertificatesAdapter type'),
};
$dbForProject->decreaseDocumentAttribute(
'topics',
@@ -389,7 +389,7 @@ class Deletes extends Action
}
/**
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param callable $getProjectDB
* @param string $hourlyUsageRetentionDatetime
* @return void
@@ -432,7 +432,7 @@ class Deletes extends Action
}
/**
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Document $document
* @return void
* @throws Authorization
@@ -442,10 +442,10 @@ class Deletes extends Action
* @throws Structure
* @throws Exception
*/
private function deleteProjectsByTeam(Database $dbForConsole, callable $getProjectDB, Document $document): void
private function deleteProjectsByTeam(Database $dbForPlatform, callable $getProjectDB, CertificatesAdapter $certificates, Document $document): void
{
$projects = $dbForConsole->find('projects', [
$projects = $dbForPlatform->find('projects', [
Query::equal('teamInternalId', [$document->getInternalId()])
]);
@@ -455,13 +455,13 @@ class Deletes extends Action
$deviceForBuilds = getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId());
$deviceForCache = getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId());
$this->deleteProject($dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $project);
$dbForConsole->deleteDocument('projects', $project->getId());
$this->deleteProject($dbForPlatform, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $certificates, $project);
$dbForPlatform->deleteDocument('projects', $project->getId());
}
}
/**
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param callable $getProjectDB
* @param Device $deviceForFiles
* @param Device $deviceForFunctions
@@ -473,7 +473,7 @@ class Deletes extends Action
* @throws Authorization
* @throws DatabaseException
*/
private function deleteProject(Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, Document $document): void
private function deleteProject(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, CertificatesAdapter $certificates, Document $document): void
{
$projectInternalId = $document->getInternalId();
$projectId = $document->getId();
@@ -489,35 +489,35 @@ class Deletes extends Action
$projectCollectionIds = [
...\array_keys(Config::getParam('collections', [])['projects']),
Audit::COLLECTION,
TimeLimit::COLLECTION,
Audit::COLLECTION
];
$limit = \count($projectCollectionIds) + 25;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
$sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', ''));
$projectTables = !\in_array($dsn->getHost(), $sharedTables);
$sharedTablesV1 = \in_array($dsn->getHost(), $sharedTablesV1);
$sharedTablesV2 = !$projectTables && !$sharedTablesV1;
$sharedTables = $sharedTablesV1 || $sharedTablesV2;
while (true) {
$collections = $dbForProject->listCollections($limit);
foreach ($collections as $collection) {
if ($dsn->getHost() !== System::getEnv('_APP_DATABASE_SHARED_TABLES', '') || !\in_array($collection->getId(), $projectCollectionIds)) {
try {
try {
if ($projectTables || !\in_array($collection->getId(), $projectCollectionIds)) {
$dbForProject->deleteCollection($collection->getId());
} catch (Throwable $e) {
Console::error('Error deleting '.$collection->getId().' '.$e->getMessage());
/**
* Ignore junction tables;
*/
if (!preg_match('/^_\d+_\d+$/', $collection->getId())) {
throw $e;
}
} else {
$this->deleteByGroup($collection->getId(), [], database: $dbForProject);
}
} else {
$this->deleteByGroup($collection->getId(), [], database: $dbForProject);
} catch (Throwable $e) {
Console::error('Error deleting '.$collection->getId().' '.$e->getMessage());
}
}
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
if ($sharedTables) {
$collectionsIds = \array_map(fn ($collection) => $collection->getId(), $collections);
if (empty(\array_diff($collectionsIds, $projectCollectionIds))) {
@@ -531,50 +531,57 @@ class Deletes extends Action
// Delete Platforms
$this->deleteByGroup('platforms', [
Query::equal('projectInternalId', [$projectInternalId])
], $dbForConsole);
], $dbForPlatform);
// Delete project and function rules
$this->deleteByGroup('rules', [
Query::equal('projectInternalId', [$projectInternalId])
], $dbForConsole, function (Document $document) use ($dbForConsole) {
$this->deleteRule($dbForConsole, $document);
], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) {
$this->deleteRule($dbForPlatform, $document, $certificates);
});
// Delete Keys
$this->deleteByGroup('keys', [
Query::equal('projectInternalId', [$projectInternalId])
], $dbForConsole);
], $dbForPlatform);
// Delete Webhooks
$this->deleteByGroup('webhooks', [
Query::equal('projectInternalId', [$projectInternalId])
], $dbForConsole);
], $dbForPlatform);
// Delete VCS Installations
$this->deleteByGroup('installations', [
Query::equal('projectInternalId', [$projectInternalId])
], $dbForConsole);
], $dbForPlatform);
// Delete VCS Repositories
$this->deleteByGroup('repositories', [
Query::equal('projectInternalId', [$projectInternalId]),
], $dbForConsole);
], $dbForPlatform);
// Delete VCS comments
$this->deleteByGroup('vcsComments', [
Query::equal('projectInternalId', [$projectInternalId]),
], $dbForConsole);
], $dbForPlatform);
// Delete Schedules (No projectInternalId in this collection)
$this->deleteByGroup('schedules', [
Query::equal('projectId', [$projectId]),
], $dbForConsole);
], $dbForPlatform);
// Delete metadata table
if ($dsn->getHost() !== System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$dbForProject->deleteCollection('_metadata');
} else {
$this->deleteByGroup('_metadata', [], $dbForProject);
if ($projectTables) {
$dbForProject->deleteCollection(Database::METADATA);
} elseif ($sharedTablesV1) {
$this->deleteByGroup(Database::METADATA, [], $dbForProject);
} elseif ($sharedTablesV2) {
$queries = \array_map(
fn ($id) => Query::notEqual('$id', $id),
$projectCollectionIds
);
$this->deleteByGroup(Database::METADATA, $queries, $dbForProject);
}
// Delete all storage directories
@@ -641,7 +648,7 @@ class Deletes extends Action
}
/**
* @param database $dbForConsole
* @param database $dbForPlatform
* @param callable $getProjectDB
* @param string $datetime
* @return void
@@ -657,7 +664,7 @@ class Deletes extends Action
}
/**
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param callable $getProjectDB
* @return void
* @throws Exception|Throwable
@@ -675,42 +682,21 @@ class Deletes extends Action
}
/**
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param string $datetime
* @return void
* @throws Exception
*/
private function deleteRealtimeUsage(Database $dbForConsole, string $datetime): void
private function deleteRealtimeUsage(Database $dbForPlatform, string $datetime): void
{
// Delete Dead Realtime Logs
$this->deleteByGroup('realtime', [
Query::lessThan('timestamp', $datetime)
], $dbForConsole);
], $dbForPlatform);
}
/**
* @param Database $dbForConsole
* @param callable $getProjectDB
* @param string $datetime
* @return void
* @throws Exception
*/
private function deleteAbuseLogs(Document $project, callable $getProjectDB, string $abuseRetention): void
{
$projectId = $project->getId();
$dbForProject = $getProjectDB($project);
$timeLimit = new TimeLimit("", 0, 1, $dbForProject);
$abuse = new Abuse($timeLimit);
try {
$abuse->cleanup($abuseRetention);
} catch (DatabaseException $e) {
Console::error('Failed to delete abuse logs for project ' . $projectId . ': ' . $e->getMessage());
}
}
/**
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param callable $getProjectDB
* @param string $datetime
* @return void
@@ -738,7 +724,7 @@ class Deletes extends Action
* @return void
* @throws Exception
*/
private function deleteFunction(Database $dbForConsole, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, Document $document, Document $project): void
private function deleteFunction(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, CertificatesAdapter $certificates, Document $document, Document $project): void
{
$projectId = $project->getId();
$dbForProject = $getProjectDB($project);
@@ -753,8 +739,8 @@ class Deletes extends Action
Query::equal('resourceType', ['function']),
Query::equal('resourceInternalId', [$functionInternalId]),
Query::equal('projectInternalId', [$project->getInternalId()])
], $dbForConsole, function (Document $document) use ($project, $dbForConsole) {
$this->deleteRule($dbForConsole, $document);
], $dbForPlatform, function (Document $document) use ($project, $dbForPlatform, $certificates) {
$this->deleteRule($dbForPlatform, $document, $certificates);
});
/**
@@ -808,13 +794,13 @@ class Deletes extends Action
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::equal('resourceInternalId', [$functionInternalId]),
Query::equal('resourceType', ['function']),
], $dbForConsole, function (Document $document) use ($dbForConsole) {
], $dbForPlatform, function (Document $document) use ($dbForPlatform) {
$providerRepositoryId = $document->getAttribute('providerRepositoryId', '');
$projectInternalId = $document->getAttribute('projectInternalId', '');
$this->deleteByGroup('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('projectInternalId', [$projectInternalId]),
], $dbForConsole);
], $dbForPlatform);
});
/**
@@ -1036,29 +1022,18 @@ class Deletes extends Action
}
/**
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Document $document rule document
* @return void
*/
private function deleteRule(Database $dbForConsole, Document $document): void
private function deleteRule(Database $dbForPlatform, Document $document, CertificatesAdapter $certificates): void
{
$domain = $document->getAttribute('domain');
$directory = APP_STORAGE_CERTIFICATES . '/' . $domain;
$checkTraversal = realpath($directory) === $directory;
if ($checkTraversal && is_dir($directory)) {
// Delete files, so Traefik is aware of change
array_map('unlink', glob($directory . '/*.*'));
rmdir($directory);
Console::info("Deleted certificate files for {$domain}");
} else {
Console::info("No certificate files found for {$domain}");
}
$certificates->deleteCertificate($domain);
// Delete certificate document, so Appwrite is aware of change
if (isset($document['certificateId'])) {
$dbForConsole->deleteDocument('certificates', $document['certificateId']);
$dbForPlatform->deleteDocument('certificates', $document['certificateId']);
}
}
@@ -1079,21 +1054,21 @@ class Deletes extends Action
}
/**
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param callable $getProjectDB
* @param Document $document
* @param Document $project
* @return void
* @throws Exception
*/
private function deleteInstallation(Database $dbForConsole, callable $getProjectDB, Document $document, Document $project): void
private function deleteInstallation(Database $dbForPlatform, callable $getProjectDB, Document $document, Document $project): void
{
$dbForProject = $getProjectDB($project);
$this->listByGroup('functions', [
Query::equal('installationInternalId', [$document->getInternalId()])
], $dbForProject, function ($function) use ($dbForProject, $dbForConsole) {
$dbForConsole->deleteDocument('repositories', $function->getAttribute('repositoryId'));
], $dbForProject, function ($function) use ($dbForProject, $dbForPlatform) {
$dbForPlatform->deleteDocument('repositories', $function->getAttribute('repositoryId'));
$function = $function
->setAttribute('installationId', '')
+51 -27
View File
@@ -16,7 +16,6 @@ use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\Restricted;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Helpers\ID;
use Utopia\Logger\Log;
use Utopia\Migration\Destination;
use Utopia\Migration\Destinations\Appwrite as DestinationAppwrite;
use Utopia\Migration\Exception as MigrationException;
@@ -33,10 +32,12 @@ class Migrations extends Action
{
protected Database $dbForProject;
protected Database $dbForConsole;
protected Database $dbForPlatform;
protected Document $project;
protected $logError;
public static function getName(): string
{
return 'migrations';
@@ -51,15 +52,15 @@ class Migrations extends Action
->desc('Migrations worker')
->inject('message')
->inject('dbForProject')
->inject('dbForConsole')
->inject('log')
->callback(fn (Message $message, Database $dbForProject, Database $dbForConsole, Log $log) => $this->action($message, $dbForProject, $dbForConsole, $log));
->inject('dbForPlatform')
->inject('logError')
->callback(fn (Message $message, Database $dbForProject, Database $dbForPlatform, callable $logError) => $this->action($message, $dbForProject, $dbForPlatform, $logError));
}
/**
* @throws Exception
*/
public function action(Message $message, Database $dbForProject, Database $dbForConsole, Log $log): void
public function action(Message $message, Database $dbForProject, Database $dbForPlatform, callable $logError): void
{
$payload = $message->getPayload() ?? [];
@@ -76,8 +77,9 @@ class Migrations extends Action
}
$this->dbForProject = $dbForProject;
$this->dbForConsole = $dbForConsole;
$this->dbForPlatform = $dbForPlatform;
$this->project = $project;
$this->logError = $logError;
/**
* Handle Event execution.
@@ -86,10 +88,7 @@ class Migrations extends Action
return;
}
$log->addTag('migrationId', $migration->getId());
$log->addTag('projectId', $project->getId());
$this->processMigration($migration, $log);
$this->processMigration($migration);
}
/**
@@ -198,7 +197,7 @@ class Migrations extends Action
*/
protected function removeAPIKey(Document $apiKey): void
{
$this->dbForConsole->deleteDocument('keys', $apiKey->getId());
$this->dbForPlatform->deleteDocument('keys', $apiKey->getId());
}
/**
@@ -245,8 +244,8 @@ class Migrations extends Action
'secret' => $generatedSecret,
]);
$this->dbForConsole->createDocument('keys', $key);
$this->dbForConsole->purgeCachedDocument('projects', $project->getId());
$this->dbForPlatform->createDocument('keys', $key);
$this->dbForPlatform->purgeCachedDocument('projects', $project->getId());
return $key;
}
@@ -259,10 +258,10 @@ class Migrations extends Action
* @throws \Utopia\Database\Exception
* @throws Exception
*/
protected function processMigration(Document $migration, Log $log): void
protected function processMigration(Document $migration): void
{
$project = $this->project;
$projectDocument = $this->dbForConsole->getDocument('projects', $project->getId());
$projectDocument = $this->dbForPlatform->getDocument('projects', $project->getId());
$tempAPIKey = $this->generateAPIKey($projectDocument);
$transfer = $source = $destination = null;
@@ -285,8 +284,6 @@ class Migrations extends Action
$migration->setAttribute('status', 'processing');
$this->updateMigrationDocument($migration, $projectDocument);
$log->addTag('type', $migration->getAttribute('source'));
$source = $this->processSource($migration);
$destination = $this->processDestination($migration, $tempAPIKey->getAttribute('secret'));
@@ -324,7 +321,6 @@ class Migrations extends Action
$errorMessages = [];
foreach ($sourceErrors as $error) {
/** @var $sourceErrors $error */
$message = "Error occurred while fetching '{$error->getResourceName()}:{$error->getResourceId()}' from source with message: '{$error->getMessage()}'";
if ($error->getPrevious()) {
$message .= " Message: ".$error->getPrevious()->getMessage() . " File: ".$error->getPrevious()->getFile() . " Line: ".$error->getPrevious()->getLine();
@@ -344,7 +340,6 @@ class Migrations extends Action
}
$migration->setAttribute('errors', $errorMessages);
$log->addExtra('migrationErrors', json_encode($errorMessages));
$this->updateMigrationDocument($migration, $projectDocument);
return;
@@ -359,7 +354,12 @@ class Migrations extends Action
if (! $migration->isEmpty()) {
$migration->setAttribute('status', 'failed');
$migration->setAttribute('stage', 'finished');
$migration->setAttribute('errors', [$th->getMessage()]);
call_user_func($this->logError, $th, 'appwrite-worker', 'appwrite-queue-'.self::getName(), [
'migrationId' => $migration->getId(),
'source' => $migration->getAttribute('source') ?? '',
'destination' => $migration->getAttribute('destination') ?? '',
]);
return;
}
@@ -379,7 +379,6 @@ class Migrations extends Action
}
$migration->setAttribute('errors', $errorMessages);
$log->addTag('migrationErrors', json_encode($errorMessages));
}
} finally {
if (! $tempAPIKey->isEmpty()) {
@@ -391,15 +390,40 @@ class Migrations extends Action
if ($migration->getAttribute('status', '') === 'failed') {
Console::error('Migration('.$migration->getInternalId().':'.$migration->getId().') failed, Project('.$this->project->getInternalId().':'.$this->project->getId().')');
$destination->error();
$source->error();
if ($destination) {
$destination->error();
throw new Exception('Migration failed');
foreach ($destination->getErrors() as $error) {
/** @var MigrationException $error */
call_user_func($this->logError, $error, 'appwrite-worker', 'appwrite-queue-' . self::getName(), [
'migrationId' => $migration->getId(),
'source' => $migration->getAttribute('source') ?? '',
'destination' => $migration->getAttribute('destination') ?? '',
'resourceName' => $error->getResourceName(),
'resourceGroup' => $error->getResourceGroup()
]);
}
}
if ($source) {
$source->error();
foreach ($source->getErrors() as $error) {
/** @var MigrationException $error */
call_user_func($this->logError, $error, 'appwrite-worker', 'appwrite-queue-' . self::getName(), [
'migrationId' => $migration->getId(),
'source' => $migration->getAttribute('source') ?? '',
'destination' => $migration->getAttribute('destination') ?? '',
'resourceName' => $error->getResourceName(),
'resourceGroup' => $error->getResourceGroup()
]);
}
}
}
if ($migration->getAttribute('status', '') === 'completed') {
$destination->success();
$source->success();
$destination?->success();
$source?->success();
}
}
}
+18 -6
View File
@@ -15,9 +15,10 @@ class Usage extends Action
{
private array $stats = [];
private int $lastTriggeredTime = 0;
private int $aggregationInterval = 20;
private int $keys = 0;
private const INFINITY_PERIOD = '_inf_';
private const KEYS_THRESHOLD = 10000;
private const KEYS_THRESHOLD = 20000;
public static function getName(): string
{
@@ -39,6 +40,7 @@ class Usage extends Action
$this->action($message, $getProjectDB, $queueForUsageDump);
});
$this->aggregationInterval = (int) System::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', '20');
$this->lastTriggeredTime = time();
}
@@ -56,10 +58,15 @@ class Usage extends Action
if (empty($payload)) {
throw new Exception('Missing payload');
}
//Todo Figure out way to preserve keys when the container is being recreated @shimonewman
$aggregationInterval = (int) System::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', '20');
$project = new Document($payload['project'] ?? []);
$document = $payload['project'] ?? [];
$project = new Document($document);
if (empty($project->getAttribute('database'))) {
var_dump($payload);
return;
}
$projectId = $project->getInternalId();
foreach ($payload['reduce'] ?? [] as $document) {
if (empty($document)) {
@@ -74,7 +81,12 @@ class Usage extends Action
);
}
$this->stats[$projectId]['project'] = $project;
$this->stats[$projectId]['project'] = [
'$id' => $project->getId(),
'$internalId' => $project->getInternalId(),
'database' => $project->getAttribute('database'),
];
$this->stats[$projectId]['receivedAt'] = DateTime::now();
foreach ($payload['metrics'] ?? [] as $metric) {
$this->keys++;
@@ -89,7 +101,7 @@ class Usage extends Action
// If keys crossed threshold or X time passed since the last send and there are some keys in the array ($this->stats)
if (
$this->keys >= self::KEYS_THRESHOLD ||
(time() - $this->lastTriggeredTime > $aggregationInterval && $this->keys > 0)
(time() - $this->lastTriggeredTime > $this->aggregationInterval && $this->keys > 0)
) {
Console::warning('[' . DateTime::now() . '] Aggregated ' . $this->keys . ' keys');
+22 -3
View File
@@ -57,16 +57,35 @@ class UsageDump extends Action
throw new Exception('Missing payload');
}
// TODO: rename both usage workers @shimonewman
foreach ($payload['stats'] ?? [] as $stats) {
$project = new Document($stats['project'] ?? []);
//$project = new Document($stats['project'] ?? []);
/**
* Start temp bug fallback
*/
$document = $stats['project'] ?? [];
if (!empty($document['$uid'])) {
$document['$id'] = $document['$uid'];
}
$project = new Document($document);
if (empty($project->getAttribute('database'))) {
continue;
}
/**
* End temp bug fallback
*/
$numberOfKeys = !empty($stats['keys']) ? count($stats['keys']) : 0;
$receivedAt = $stats['receivedAt'] ?? 'NONE';
if ($numberOfKeys === 0) {
continue;
}
console::log('[' . DateTime::now() . '] ProjectId [' . $project->getInternalId() . '] ReceivedAt [' . $receivedAt . '] ' . $numberOfKeys . ' keys');
console::log('['.DateTime::now().'] Id: '.$project->getId(). ' InternalId: '.$project->getInternalId(). ' Db: '.$project->getAttribute('database').' ReceivedAt: '.$receivedAt. ' Keys: '.$numberOfKeys);
try {
$dbForProject = $getProjectDB($project);
+36 -19
View File
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Event\Mail;
use Appwrite\Event\Usage;
use Appwrite\Template\Template;
use Exception;
use Utopia\Database\Database;
@@ -31,21 +32,22 @@ class Webhooks extends Action
$this
->desc('Webhooks worker')
->inject('message')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForMails')
->inject('queueForUsage')
->inject('log')
->callback(fn (Message $message, Database $dbForConsole, Mail $queueForMails, Log $log) => $this->action($message, $dbForConsole, $queueForMails, $log));
->callback(fn (Message $message, Database $dbForPlatform, Mail $queueForMails, Usage $queueForUsage, Log $log) => $this->action($message, $dbForPlatform, $queueForMails, $queueForUsage, $log));
}
/**
* @param Message $message
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Mail $queueForMails
* @param Log $log
* @return void
* @throws Exception
*/
public function action(Message $message, Database $dbForConsole, Mail $queueForMails, Log $log): void
public function action(Message $message, Database $dbForPlatform, Mail $queueForMails, Usage $queueForUsage, Log $log): void
{
$this->errors = [];
$payload = $message->getPayload() ?? [];
@@ -56,14 +58,15 @@ class Webhooks extends Action
$events = $payload['events'];
$webhookPayload = json_encode($payload['payload']);
$project = new Document($payload['project']);
$user = new Document($payload['user'] ?? []);
$project = new Document($payload['project']);
$project = $dbForPlatform->getDocument('projects', $project->getId());
$log->addTag('projectId', $project->getId());
foreach ($project->getAttribute('webhooks', []) as $webhook) {
if (array_intersect($webhook->getAttribute('events', []), $events)) {
$this->execute($events, $webhookPayload, $webhook, $user, $project, $dbForConsole, $queueForMails);
$this->execute($events, $webhookPayload, $webhook, $user, $project, $dbForPlatform, $queueForMails, $queueForUsage);
}
}
@@ -78,11 +81,11 @@ class Webhooks extends Action
* @param Document $webhook
* @param Document $user
* @param Document $project
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Mail $queueForMails
* @return void
*/
private function execute(array $events, string $payload, Document $webhook, Document $user, Document $project, Database $dbForConsole, Mail $queueForMails): void
private function execute(array $events, string $payload, Document $webhook, Document $user, Document $project, Database $dbForPlatform, Mail $queueForMails, Usage $queueForUsage): void
{
if ($webhook->getAttribute('enabled') !== true) {
return;
@@ -138,8 +141,8 @@ class Webhooks extends Action
\curl_close($ch);
if (!empty($curlError) || $statusCode >= 400) {
$dbForConsole->increaseDocumentAttribute('webhooks', $webhook->getId(), 'attempts', 1);
$webhook = $dbForConsole->getDocument('webhooks', $webhook->getId());
$dbForPlatform->increaseDocumentAttribute('webhooks', $webhook->getId(), 'attempts', 1);
$webhook = $dbForPlatform->getDocument('webhooks', $webhook->getId());
$attempts = $webhook->getAttribute('attempts');
$logs = '';
@@ -158,18 +161,32 @@ class Webhooks extends Action
if ($attempts >= \intval(System::getEnv('_APP_WEBHOOK_MAX_FAILED_ATTEMPTS', '10'))) {
$webhook->setAttribute('enabled', false);
$this->sendEmailAlert($attempts, $statusCode, $webhook, $project, $dbForConsole, $queueForMails);
$this->sendEmailAlert($attempts, $statusCode, $webhook, $project, $dbForPlatform, $queueForMails);
}
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
$dbForConsole->purgeCachedDocument('projects', $project->getId());
$dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$this->errors[] = $logs;
$queueForUsage
->addMetric(METRIC_WEBHOOKS_FAILED, 1)
->addMetric(str_replace('{webhookInternalId}', $webhook->getInternalId(), METRIC_WEBHOOK_ID_FAILED), 1)
;
} else {
$webhook->setAttribute('attempts', 0); // Reset attempts on success
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
$dbForConsole->purgeCachedDocument('projects', $project->getId());
$dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$queueForUsage
->addMetric(METRIC_WEBHOOKS_SENT, 1)
->addMetric(str_replace('{webhookInternalId}', $webhook->getInternalId(), METRIC_WEBHOOK_ID_SENT), 1)
;
}
$queueForUsage
->setProject($project)
->trigger();
}
/**
@@ -177,20 +194,20 @@ class Webhooks extends Action
* @param mixed $statusCode
* @param Document $webhook
* @param Document $project
* @param Database $dbForConsole
* @param Database $dbForPlatform
* @param Mail $queueForMails
* @return void
*/
public function sendEmailAlert(int $attempts, mixed $statusCode, Document $webhook, Document $project, Database $dbForConsole, Mail $queueForMails): void
public function sendEmailAlert(int $attempts, mixed $statusCode, Document $webhook, Document $project, Database $dbForPlatform, Mail $queueForMails): void
{
$memberships = $dbForConsole->find('memberships', [
$memberships = $dbForPlatform->find('memberships', [
Query::equal('teamInternalId', [$project->getAttribute('teamInternalId')]),
Query::limit(APP_LIMIT_SUBQUERY)
]);
$userIds = array_column(\array_map(fn ($membership) => $membership->getArrayCopy(), $memberships), 'userId');
$users = $dbForConsole->find('users', [
$users = $dbForPlatform->find('users', [
Query::equal('$id', $userIds),
]);
@@ -323,6 +323,7 @@ class OpenAPI3 extends Format
case 'Utopia\Validator\JSON':
case 'Utopia\Validator\Mock':
case 'Utopia\Validator\Assoc':
case 'Appwrite\Functions\Validator\Payload':
$param['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
$node['schema']['type'] = 'object';
$node['schema']['x-example'] = '{}';
@@ -349,6 +349,7 @@ class Swagger2 extends Format
case 'Utopia\Validator\JSON':
case 'Utopia\Validator\Mock':
case 'Utopia\Validator\Assoc':
case 'Appwrite\Functions\Validator\Payload':
$node['type'] = 'object';
$node['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
$node['x-example'] = '{}';
+1 -1
View File
@@ -94,7 +94,7 @@ class Func extends Model
])
->addRule('schedule', [
'type' => self::TYPE_STRING,
'description' => 'Function execution schedult in CRON format.',
'description' => 'Function execution schedule in CRON format.',
'default' => '',
'example' => '5 4 * * *',
])
@@ -1362,7 +1362,7 @@ class DatabasesCustomServerTest extends Scope
]);
$this->assertEquals(400, $tooWide['headers']['status-code']);
$this->assertEquals('Attribute limit exceeded', $tooWide['body']['message']);
$this->assertEquals('attribute_limit_exceeded', $tooWide['body']['type']);
}
public function testIndexLimitException()
@@ -3727,11 +3727,11 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'teamId' => ID::unique(),
'name' => 'Amating Team',
'name' => 'Amazing Team',
]);
$this->assertEquals(201, $team['headers']['status-code']);
$this->assertEquals('Amating Team', $team['body']['name']);
$this->assertEquals('Amazing Team', $team['body']['name']);
$this->assertNotEmpty($team['body']['$id']);
$teamId = $team['body']['$id'];
@@ -3794,6 +3794,115 @@ class ProjectsConsoleClientTest extends Scope
return $data;
}
public function testDeleteSharedProject(): void
{
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'teamId' => ID::unique(),
'name' => 'Amazing Team',
]);
$teamId = $team['body']['$id'];
// Ensure deleting one project does not affect another project
$project1 = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Amazing Project 1',
'teamId' => $teamId,
'region' => 'default'
]);
$project2 = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Amazing Project 2',
'teamId' => $teamId,
'region' => 'default'
]);
$project1Id = $project1['body']['$id'];
$project2Id = $project2['body']['$id'];
// Create user in each project
$key1 = $this->client->call(Client::METHOD_POST, '/projects/' . $project1Id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Key Test',
'scopes' => ['users.read', 'users.write'],
]);
$user1 = $this->client->call(Client::METHOD_POST, '/users', [
'content-type' => 'application/json',
'x-appwrite-project' => $project1Id,
'x-appwrite-key' => $key1['body']['secret'],
], [
'userId' => ID::unique(),
'email' => 'test1@appwrite.io',
'password' => 'password',
]);
$this->assertEquals(201, $user1['headers']['status-code']);
$key2 = $this->client->call(Client::METHOD_POST, '/projects/' . $project2Id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Key Test',
'scopes' => ['users.read', 'users.write'],
]);
$user2 = $this->client->call(Client::METHOD_POST, '/users', [
'content-type' => 'application/json',
'x-appwrite-project' => $project2Id,
'x-appwrite-key' => $key2['body']['secret'],
], [
'userId' => ID::unique(),
'email' => 'test2@appwrite.io',
'password' => 'password',
]);
$this->assertEquals(201, $user2['headers']['status-code']);
// Delete project 1
$project1 = $this->client->call(Client::METHOD_DELETE, '/projects/' . $project1Id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $project1['headers']['status-code']);
\sleep(3);
// Ensure project 2 user is still there
$user2 = $this->client->call(Client::METHOD_GET, '/users/' . $user2['body']['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $project2Id,
'x-appwrite-key' => $key2['body']['secret'],
]);
$this->assertEquals(200, $user2['headers']['status-code']);
// Create another user in project 2 in case read hits stale cache
$user3 = $this->client->call(Client::METHOD_POST, '/users', [
'content-type' => 'application/json',
'x-appwrite-project' => $project2Id,
'x-appwrite-key' => $key2['body']['secret'],
], [
'userId' => ID::unique(),
'email' => 'test3@appwrite.io'
]);
$this->assertEquals(201, $user3['headers']['status-code']);
}
/**
* @depends testCreateProject
*/
@@ -111,6 +111,30 @@ class RealtimeCustomClientTest extends Scope
$client->close();
}
public function testPingPong()
{
$client = $this->getWebsocket(['files'], [
'origin' => 'http://localhost'
]);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('connected', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertCount(1, $response['data']['channels']);
$this->assertContains('files', $response['data']['channels']);
$client->send(\json_encode([
'type' => 'ping'
]));
$response = json_decode($client->receive(), true);
$this->assertEquals('pong', $response['type']);
$client->close();
}
public function testManualAuthentication()
{
$user = $this->getUser();
+72 -2
View File
@@ -559,6 +559,76 @@ trait TeamsBaseClient
return $data;
}
/**
* @depends testCreateTeam
*/
public function testUpdateMembershipWithSession(array $data): void
{
$teamUid = $data['teamUid'] ?? '';
// create user
$response = $this->client->call(Client::METHOD_POST, '/account', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'userId' => 'unique()',
'email' => uniqid() . 'foe@localhost.test',
'password' => 'password',
'name' => 'test'
]);
$this->assertEquals(201, $response['headers']['status-code']);
$user = $response['body'];
// create session
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'email' => $user['email'],
'password' => 'password'
]);
$this->assertEquals(201, $response['headers']['status-code']);
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
$response = $this->client->call(Client::METHOD_POST, '/teams/' . $teamUid . '/memberships', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'email' => $user['email'],
'roles' => ['developer'],
'url' => 'http://localhost:5000/join-us#title'
]);
$this->assertEquals(201, $response['headers']['status-code']);
$lastEmail = $this->getLastEmail();
$secret = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
$membershipUid = substr($lastEmail['text'], strpos($lastEmail['text'], '?membershipId=', 0) + 14, 20);
$userUid = substr($lastEmail['text'], strpos($lastEmail['text'], '&userId=', 0) + 8, 20);
$response = $this->client->call(Client::METHOD_PATCH, '/teams/' . $teamUid . '/memberships/' . $membershipUid . '/status', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
], [
'secret' => $secret,
'userId' => $userUid,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertNotEmpty($response['body']['userId']);
$this->assertNotEmpty($response['body']['teamId']);
$this->assertCount(1, $response['body']['roles']);
$this->assertEmpty($response['cookies']);
}
/**
* @depends testUpdateTeamMembership
*/
@@ -648,7 +718,7 @@ trait TeamsBaseClient
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(3, $response['body']['total']);
$this->assertEquals(4, $response['body']['total']);
$ownerMembershipUid = $response['body']['memberships'][0]['$id'];
@@ -703,7 +773,7 @@ trait TeamsBaseClient
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(2, $response['body']['total']);
$this->assertEquals(3, $response['body']['total']);
/**
* Test for when the owner tries to delete their membership
@@ -83,6 +83,38 @@ class TeamsCustomClientTest extends Scope
$this->assertNotEmpty($response['body']['memberships'][0]['userName']);
$this->assertNotEmpty($response['body']['memberships'][0]['userEmail']);
$this->assertArrayHasKey('mfa', $response['body']['memberships'][0]);
/**
* Update project settings to show only MFA
*/
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $this->getProject()['$id'] . '/auth/memberships-privacy', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'userName' => false,
'userEmail' => false,
'mfa' => true,
]);
$this->assertEquals(200, $response['headers']['status-code']);
/**
* Test that sensitive fields are not shown
*/
$response = $this->client->call(Client::METHOD_GET, '/teams/' . $teamUid . '/memberships', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['total']);
$this->assertNotEmpty($response['body']['memberships'][0]['$id']);
// Assert that sensitive fields are present
$this->assertEmpty($response['body']['memberships'][0]['userName']);
$this->assertEmpty($response['body']['memberships'][0]['userEmail']);
$this->assertArrayHasKey('mfa', $response['body']['memberships'][0]);
}
/**
+8
View File
@@ -310,6 +310,14 @@ trait UsersBase
$this->assertNotEmpty($session['secret']);
$this->assertNotEmpty($session['expire']);
$this->assertEquals('server', $session['provider']);
$response = $this->client->call(Client::METHOD_GET, '/account', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-session' => $session['secret']
]);
$this->assertEquals(200, $response['headers']['status-code']);
}