diff --git a/.env b/.env index 4213daaf3a..f51a3f789c 100644 --- a/.env +++ b/.env @@ -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 \ No newline at end of file +_APP_GEO_SECRET=geo-secret-key +_APP_FUNCTIONS_CREATION_ABUSE_LIMIT=5000 diff --git a/.github/workflows/sdk-preview.yml b/.github/workflows/sdk-preview.yml new file mode 100644 index 0000000000..92b4f454cb --- /dev/null +++ b/.github/workflows/sdk-preview.yml @@ -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 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 29aa9b0581..3ab240c1b9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 diff --git a/.gitignore b/.gitignore index 5ae03e2a56..a68af84071 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ dev/yasd_init.php .phpunit.result.cache Makefile appwrite.json +/.zed/ diff --git a/CHANGES.md b/CHANGES.md index 9b6172eeab..62db3d525e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b92361e51a..e89aa369cf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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: diff --git a/Dockerfile b/Dockerfile index 07295f4afb..41810f5dc4 100755 --- a/Dockerfile +++ b/Dockerfile @@ -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" diff --git a/README-CN.md b/README-CN.md index a5eac1be03..92a9bf9806 100644 --- a/README-CN.md +++ b/README-CN.md @@ -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 的本机主机上完成安装后,服务器可能需要几分钟才能启动。 diff --git a/README.md b/README.md index a9856a7310..ab57e65c4c 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ docker run -it --rm \ --volume /var/run/docker.sock:/var/run/docker.sock \ --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \ --entrypoint="install" \ - appwrite/appwrite:1.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. diff --git a/app/cli.php b/app/cli.php index 23502ec402..47f4525f0b 100644 --- a/app/cli.php +++ b/app/cli.php @@ -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()); diff --git a/app/config/errors.php b/app/config/errors.php index 3afec4faaf..f09d1596eb 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -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 => [ diff --git a/app/config/function-templates.php b/app/config/function-templates.php index 762c33dd9a..fec9d2227f 100644 --- a/app/config/function-templates.php +++ b/app/config/function-templates.php @@ -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', diff --git a/app/config/platforms.php b/app/config/platforms.php index e54fb0a073..167e55651c 100644 --- a/app/config/platforms.php +++ b/app/config/platforms.php @@ -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, diff --git a/app/config/specs/open-api3-latest-client.json b/app/config/specs/open-api3-latest-client.json index e3cd909b4e..af7303c985 100644 --- a/app/config/specs/open-api3-latest-client.json +++ b/app/config/specs/open-api3-latest-client.json @@ -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, diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index 4e97fecc30..6878909f64 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -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": "" - } - }, - "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": "" - }, - "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": { diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index e84b751743..8900d6ddf2 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -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": { diff --git a/app/config/specs/swagger2-latest-client.json b/app/config/specs/swagger2-latest-client.json index b1b9ce8dca..77025ec042 100644 --- a/app/config/specs/swagger2-latest-client.json +++ b/app/config/specs/swagger2-latest-client.json @@ -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, diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index c7b6bac2d4..0ef937050c 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -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": "" - } - }, - "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": "", - "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": { diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index 2c8e80c65e..f5b1f24a22 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -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": { diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index dadd9da5e3..f73f8a148a 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -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'] ?? ''; diff --git a/app/controllers/api/console.php b/app/controllers/api/console.php index eeb823a3d3..1b307b1d14 100644 --- a/app/controllers/api/console.php +++ b/app/controllers/api/console.php @@ -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']; diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 2048271895..954ae6e892 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -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(); }); diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index b1eed84f52..a65e260d09 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -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(); }); diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 9e52fa10b1..9aa737dc23 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -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 :.', 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 } diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index bebb6ebaea..29e46505f5 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -1,17 +1,12 @@ 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') diff --git a/app/controllers/api/project.php b/app/controllers/api/project.php index 6053326308..45c8f2c32b 100644 --- a/app/controllers/api/project.php +++ b/app/controllers/api/project.php @@ -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); diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index df8b1cb07b..cbda6ad544 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -16,7 +16,6 @@ use Appwrite\Utopia\Database\Validator\Queries\Projects; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use PHPMailer\PHPMailer\PHPMailer; -use Utopia\Abuse\Adapters\Database\TimeLimit; use Utopia\App; use Utopia\Audit\Audit; use Utopia\Cache\Cache; @@ -83,13 +82,13 @@ App::post('/v1/projects') ->param('legalTaxId', '', new Text(256), 'Project legal Tax ID. Max length: 256 chars.', true) ->inject('request') ->inject('response') - ->inject('dbForConsole') + ->inject('dbForPlatform') ->inject('cache') ->inject('pools') ->inject('hooks') - ->action(function (string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Request $request, Response $response, Database $dbForConsole, Cache $cache, Group $pools, Hooks $hooks) { + ->action(function (string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Request $request, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks) { - $team = $dbForConsole->getDocument('teams', $teamId); + $team = $dbForPlatform->getDocument('teams', $teamId); if ($team->isEmpty()) { throw new Exception(Exception::TEAM_NOT_FOUND); @@ -122,6 +121,10 @@ App::post('/v1/projects') $projectId = ($projectId == 'unique()') ? ID::unique() : $projectId; + if ($projectId === 'console') { + throw new Exception(Exception::PROJECT_RESERVED_PROJECT, "'console' is a reserved project."); + } + $databases = Config::getParam('pools-database', []); $databaseOverride = System::getEnv('_APP_DATABASE_OVERRIDE'); @@ -132,16 +135,14 @@ App::post('/v1/projects') $dsn = $databases[array_rand($databases)]; } - if ($projectId === 'console') { - throw new Exception(Exception::PROJECT_RESERVED_PROJECT, "'console' is a reserved project."); - } - // TODO: Temporary until all projects are using shared tables. - if ($dsn === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { + $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + + if (\in_array($dsn, $sharedTables)) { $schema = 'appwrite'; $database = 'appwrite'; $namespace = System::getEnv('_APP_DATABASE_SHARED_NAMESPACE', ''); - $dsn = $schema . '://' . System::getEnv('_APP_DATABASE_SHARED_TABLES', '') . '?database=' . $database; + $dsn = $schema . '://' . $dsn . '?database=' . $database; if (!empty($namespace)) { $dsn .= '&namespace=' . $namespace; @@ -149,7 +150,7 @@ App::post('/v1/projects') } try { - $project = $dbForConsole->createDocument('projects', new Document([ + $project = $dbForPlatform->createDocument('projects', new Document([ '$id' => $projectId, '$permissions' => [ Permission::read(Role::team(ID::custom($teamId))), @@ -195,47 +196,78 @@ App::post('/v1/projects') $adapter = $pools->get($dsn->getHost())->pop()->getResource(); $dbForProject = new Database($adapter, $cache); + $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + $sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', '')); - if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) { - $dbForProject - ->setSharedTables(true) - ->setTenant($project->getInternalId()) - ->setNamespace($dsn->getParam('namespace')); - } else { - $dbForProject - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getInternalId()); - } + $projectTables = !\in_array($dsn->getHost(), $sharedTables); + $sharedTablesV1 = \in_array($dsn->getHost(), $sharedTablesV1); + $sharedTablesV2 = !$projectTables && !$sharedTablesV1; + $sharedTables = $sharedTablesV1 || $sharedTablesV2; - $dbForProject->create(); - - $audit = new Audit($dbForProject); - $audit->setup(); - - $abuse = new TimeLimit('', 0, 1, $dbForProject); - $abuse->setup(); - - /** @var array $collections */ - $collections = Config::getParam('collections', [])['projects'] ?? []; - - foreach ($collections as $key => $collection) { - if (($collection['$collection'] ?? '') !== Database::METADATA) { - continue; + if (!$sharedTablesV2) { + if ($sharedTables) { + $dbForProject + ->setSharedTables(true) + ->setTenant($sharedTablesV1 ? $project->getInternalId() : null) + ->setNamespace($dsn->getParam('namespace')); + } else { + $dbForProject + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getInternalId()); } - $attributes = \array_map(function (array $attribute) { - return new Document($attribute); - }, $collection['attributes']); - - $indexes = \array_map(function (array $index) { - return new Document($index); - }, $collection['indexes']); + $create = true; try { - $dbForProject->createCollection($key, $attributes, $indexes); + $dbForProject->create(); } catch (Duplicate) { - // Collection already exists + $create = false; + } + + if ($create || $projectTables) { + $audit = new Audit($dbForProject); + $audit->setup(); + } + + if (!$create && $sharedTablesV1) { + $attributes = \array_map(fn ($attribute) => new Document($attribute), Audit::ATTRIBUTES); + $indexes = \array_map(fn (array $index) => new Document($index), Audit::INDEXES); + $dbForProject->createDocument(Database::METADATA, new Document([ + '$id' => ID::custom('audit'), + '$permissions' => [Permission::create(Role::any())], + 'name' => 'audit', + 'attributes' => $attributes, + 'indexes' => $indexes, + 'documentSecurity' => true + ])); + } + + if ($create || $sharedTablesV1) { + /** @var array $collections */ + $collections = Config::getParam('collections', [])['projects'] ?? []; + + foreach ($collections as $key => $collection) { + if (($collection['$collection'] ?? '') !== Database::METADATA) { + continue; + } + + $attributes = \array_map(fn ($attribute) => new Document($attribute), $collection['attributes']); + $indexes = \array_map(fn (array $index) => new Document($index), $collection['indexes']); + + try { + $dbForProject->createCollection($key, $attributes, $indexes); + } catch (Duplicate) { + $dbForProject->createDocument(Database::METADATA, new Document([ + '$id' => ID::custom($key), + '$permissions' => [Permission::create(Role::any())], + 'name' => $key, + 'attributes' => $attributes, + 'indexes' => $indexes, + 'documentSecurity' => true + ])); + } + } } } @@ -261,8 +293,8 @@ App::get('/v1/projects') ->param('queries', [], new Projects(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Projects::ALLOWED_ATTRIBUTES), true) ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('response') - ->inject('dbForConsole') - ->action(function (array $queries, string $search, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (array $queries, string $search, Response $response, Database $dbForPlatform) { try { $queries = Query::parseQueries($queries); @@ -290,7 +322,7 @@ App::get('/v1/projects') } $projectId = $cursor->getValue(); - $cursorDocument = $dbForConsole->getDocument('projects', $projectId); + $cursorDocument = $dbForPlatform->getDocument('projects', $projectId); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Project '{$projectId}' for the 'cursor' value not found."); @@ -302,8 +334,8 @@ App::get('/v1/projects') $filterQueries = Query::groupByType($queries)['filters']; $response->dynamic(new Document([ - 'projects' => $dbForConsole->find('projects', $queries), - 'total' => $dbForConsole->count('projects', $filterQueries, APP_LIMIT_COUNT), + 'projects' => $dbForPlatform->find('projects', $queries), + 'total' => $dbForPlatform->count('projects', $filterQueries, APP_LIMIT_COUNT), ]), Response::MODEL_PROJECT_LIST); }); @@ -319,10 +351,10 @@ App::get('/v1/projects/:projectId') ->label('sdk.response.model', Response::MODEL_PROJECT) ->param('projectId', '', new UID(), 'Project unique ID.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -353,16 +385,16 @@ App::patch('/v1/projects/:projectId') ->param('legalAddress', '', new Text(256), 'Project legal address. Max length: 256 chars.', true) ->param('legalTaxId', '', new Text(256), 'Project legal tax ID. Max length: 256 chars.', true) ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $name, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $name, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - $project = $dbForConsole->updateDocument('projects', $project->getId(), $project + $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project ->setAttribute('name', $name) ->setAttribute('description', $description) ->setAttribute('logo', $logo) @@ -391,11 +423,11 @@ App::patch('/v1/projects/:projectId/team') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('teamId', '', new UID(), 'Team ID of the team to transfer project to.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $teamId, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $teamId, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); - $team = $dbForConsole->getDocument('teams', $teamId); + $project = $dbForPlatform->getDocument('projects', $projectId); + $team = $dbForPlatform->getDocument('teams', $teamId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -417,30 +449,30 @@ App::patch('/v1/projects/:projectId/team') ->setAttribute('teamId', $teamId) ->setAttribute('teamInternalId', $team->getInternalId()) ->setAttribute('$permissions', $permissions); - $project = $dbForConsole->updateDocument('projects', $project->getId(), $project); + $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project); - $installations = $dbForConsole->find('installations', [ + $installations = $dbForPlatform->find('installations', [ Query::equal('projectInternalId', [$project->getInternalId()]), ]); foreach ($installations as $installation) { $installation->getAttribute('$permissions', $permissions); - $dbForConsole->updateDocument('installations', $installation->getId(), $installation); + $dbForPlatform->updateDocument('installations', $installation->getId(), $installation); } - $repositories = $dbForConsole->find('repositories', [ + $repositories = $dbForPlatform->find('repositories', [ Query::equal('projectInternalId', [$project->getInternalId()]), ]); foreach ($repositories as $repository) { $repository->getAttribute('$permissions', $permissions); - $dbForConsole->updateDocument('repositories', $repository->getId(), $repository); + $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository); } - $vcsComments = $dbForConsole->find('vcsComments', [ + $vcsComments = $dbForPlatform->find('vcsComments', [ Query::equal('projectInternalId', [$project->getInternalId()]), ]); foreach ($vcsComments as $vcsComment) { $vcsComment->getAttribute('$permissions', $permissions); - $dbForConsole->updateDocument('vcsComments', $vcsComment->getId(), $vcsComment); + $dbForPlatform->updateDocument('vcsComments', $vcsComment->getId(), $vcsComment); } $response->dynamic($project, Response::MODEL_PROJECT); @@ -460,10 +492,10 @@ App::patch('/v1/projects/:projectId/service') ->param('service', '', new WhiteList(array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional'])), true), 'Service name.') ->param('status', null, new Boolean(), 'Service status.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $service, bool $status, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $service, bool $status, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -472,7 +504,7 @@ App::patch('/v1/projects/:projectId/service') $services = $project->getAttribute('services', []); $services[$service] = $status; - $project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('services', $services)); + $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('services', $services)); $response->dynamic($project, Response::MODEL_PROJECT); }); @@ -490,10 +522,10 @@ App::patch('/v1/projects/:projectId/service/all') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('status', null, new Boolean(), 'Service status.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, bool $status, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, bool $status, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -506,7 +538,7 @@ App::patch('/v1/projects/:projectId/service/all') $services[$service] = $status; } - $project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('services', $services)); + $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('services', $services)); $response->dynamic($project, Response::MODEL_PROJECT); }); @@ -525,10 +557,10 @@ App::patch('/v1/projects/:projectId/api') ->param('api', '', new WhiteList(array_keys(Config::getParam('apis')), true), 'API name.') ->param('status', null, new Boolean(), 'API status.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $api, bool $status, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $api, bool $status, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -537,7 +569,7 @@ App::patch('/v1/projects/:projectId/api') $apis = $project->getAttribute('apis', []); $apis[$api] = $status; - $project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('apis', $apis)); + $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('apis', $apis)); $response->dynamic($project, Response::MODEL_PROJECT); }); @@ -555,10 +587,10 @@ App::patch('/v1/projects/:projectId/api/all') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('status', null, new Boolean(), 'API status.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, bool $status, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, bool $status, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -571,7 +603,7 @@ App::patch('/v1/projects/:projectId/api/all') $apis[$api] = $status; } - $project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('apis', $apis)); + $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('apis', $apis)); $response->dynamic($project, Response::MODEL_PROJECT); }); @@ -592,10 +624,10 @@ App::patch('/v1/projects/:projectId/oauth2') ->param('secret', null, new text(512), 'Provider secret key. Max length: 512 chars.', true) ->param('enabled', null, new Boolean(), 'Provider status. Set to \'false\' to disable new session creation.', true) ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $provider, ?string $appId, ?string $secret, ?bool $enabled, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $provider, ?string $appId, ?string $secret, ?bool $enabled, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -615,7 +647,7 @@ App::patch('/v1/projects/:projectId/oauth2') $providers[$provider . 'Enabled'] = $enabled; } - $project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('oAuthProviders', $providers)); + $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('oAuthProviders', $providers)); $response->dynamic($project, Response::MODEL_PROJECT); }); @@ -633,10 +665,10 @@ App::patch('/v1/projects/:projectId/auth/session-alerts') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('alerts', false, new Boolean(true), 'Set to true to enable session emails.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, bool $alerts, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, bool $alerts, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -645,7 +677,7 @@ App::patch('/v1/projects/:projectId/auth/session-alerts') $auths = $project->getAttribute('auths', []); $auths['sessionAlerts'] = $alerts; - $dbForConsole->updateDocument('projects', $project->getId(), $project + $dbForPlatform->updateDocument('projects', $project->getId(), $project ->setAttribute('auths', $auths)); $response->dynamic($project, Response::MODEL_PROJECT); @@ -666,9 +698,9 @@ App::patch('/v1/projects/:projectId/auth/memberships-privacy') ->param('userEmail', true, new Boolean(true), 'Set to true to show email to members of a team.') ->param('mfa', true, new Boolean(true), 'Set to true to show mfa to members of a team.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, bool $userName, bool $userEmail, bool $mfa, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); + ->inject('dbForPlatform') + ->action(function (string $projectId, bool $userName, bool $userEmail, bool $mfa, Response $response, Database $dbForPlatform) { + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -680,7 +712,7 @@ App::patch('/v1/projects/:projectId/auth/memberships-privacy') $auths['membershipsUserEmail'] = $userEmail; $auths['membershipsMfa'] = $mfa; - $dbForConsole->updateDocument('projects', $project->getId(), $project + $dbForPlatform->updateDocument('projects', $project->getId(), $project ->setAttribute('auths', $auths)); $response->dynamic($project, Response::MODEL_PROJECT); @@ -699,10 +731,10 @@ App::patch('/v1/projects/:projectId/auth/limit') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('limit', false, new Range(0, APP_LIMIT_USERS), 'Set the max number of users allowed in this project. Use 0 for unlimited.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, int $limit, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, int $limit, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -711,7 +743,7 @@ App::patch('/v1/projects/:projectId/auth/limit') $auths = $project->getAttribute('auths', []); $auths['limit'] = $limit; - $dbForConsole->updateDocument('projects', $project->getId(), $project + $dbForPlatform->updateDocument('projects', $project->getId(), $project ->setAttribute('auths', $auths)); $response->dynamic($project, Response::MODEL_PROJECT); @@ -730,10 +762,10 @@ App::patch('/v1/projects/:projectId/auth/duration') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('duration', 31536000, new Range(0, 31536000), 'Project session length in seconds. Max length: 31536000 seconds.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, int $duration, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, int $duration, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -742,7 +774,7 @@ App::patch('/v1/projects/:projectId/auth/duration') $auths = $project->getAttribute('auths', []); $auths['duration'] = $duration; - $dbForConsole->updateDocument('projects', $project->getId(), $project + $dbForPlatform->updateDocument('projects', $project->getId(), $project ->setAttribute('auths', $auths)); $response->dynamic($project, Response::MODEL_PROJECT); @@ -762,10 +794,10 @@ App::patch('/v1/projects/:projectId/auth/:method') ->param('method', '', new WhiteList(\array_keys(Config::getParam('auth')), true), 'Auth Method. Possible values: ' . implode(',', \array_keys(Config::getParam('auth'))), false) ->param('status', false, new Boolean(true), 'Set the status of this auth method.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $method, bool $status, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $method, bool $status, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); $auth = Config::getParam('auth')[$method] ?? []; $authKey = $auth['key'] ?? ''; $status = ($status === '1' || $status === 'true' || $status === 1 || $status === true); @@ -777,7 +809,7 @@ App::patch('/v1/projects/:projectId/auth/:method') $auths = $project->getAttribute('auths', []); $auths[$authKey] = $status; - $project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('auths', $auths)); + $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('auths', $auths)); $response->dynamic($project, Response::MODEL_PROJECT); }); @@ -795,10 +827,10 @@ App::patch('/v1/projects/:projectId/auth/password-history') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('limit', 0, new Range(0, APP_LIMIT_USER_PASSWORD_HISTORY), 'Set the max number of passwords to store in user history. User can\'t choose a new password that is already stored in the password history list. Max number of passwords allowed in history is' . APP_LIMIT_USER_PASSWORD_HISTORY . '. Default value is 0') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, int $limit, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, int $limit, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -807,7 +839,7 @@ App::patch('/v1/projects/:projectId/auth/password-history') $auths = $project->getAttribute('auths', []); $auths['passwordHistory'] = $limit; - $dbForConsole->updateDocument('projects', $project->getId(), $project + $dbForPlatform->updateDocument('projects', $project->getId(), $project ->setAttribute('auths', $auths)); $response->dynamic($project, Response::MODEL_PROJECT); @@ -826,10 +858,10 @@ App::patch('/v1/projects/:projectId/auth/password-dictionary') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('enabled', false, new Boolean(false), 'Set whether or not to enable checking user\'s password against most commonly used passwords. Default is false.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, bool $enabled, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, bool $enabled, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -838,7 +870,7 @@ App::patch('/v1/projects/:projectId/auth/password-dictionary') $auths = $project->getAttribute('auths', []); $auths['passwordDictionary'] = $enabled; - $dbForConsole->updateDocument('projects', $project->getId(), $project + $dbForPlatform->updateDocument('projects', $project->getId(), $project ->setAttribute('auths', $auths)); $response->dynamic($project, Response::MODEL_PROJECT); @@ -857,10 +889,10 @@ App::patch('/v1/projects/:projectId/auth/personal-data') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('enabled', false, new Boolean(false), 'Set whether or not to check a password for similarity with personal data. Default is false.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, bool $enabled, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, bool $enabled, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -869,7 +901,7 @@ App::patch('/v1/projects/:projectId/auth/personal-data') $auths = $project->getAttribute('auths', []); $auths['personalDataCheck'] = $enabled; - $dbForConsole->updateDocument('projects', $project->getId(), $project + $dbForPlatform->updateDocument('projects', $project->getId(), $project ->setAttribute('auths', $auths)); $response->dynamic($project, Response::MODEL_PROJECT); @@ -888,10 +920,10 @@ App::patch('/v1/projects/:projectId/auth/max-sessions') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('limit', false, new Range(1, APP_LIMIT_USER_SESSIONS_MAX), 'Set the max number of users allowed in this project. Value allowed is between 1-' . APP_LIMIT_USER_SESSIONS_MAX . '. Default is ' . APP_LIMIT_USER_SESSIONS_DEFAULT) ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, int $limit, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, int $limit, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -900,7 +932,7 @@ App::patch('/v1/projects/:projectId/auth/max-sessions') $auths = $project->getAttribute('auths', []); $auths['maxSessions'] = $limit; - $dbForConsole->updateDocument('projects', $project->getId(), $project + $dbForPlatform->updateDocument('projects', $project->getId(), $project ->setAttribute('auths', $auths)); $response->dynamic($project, Response::MODEL_PROJECT); @@ -919,8 +951,8 @@ App::patch('/v1/projects/:projectId/auth/mock-numbers') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('numbers', '', new ArrayList(new MockNumber(), 10), 'An array of mock numbers and their corresponding verification codes (OTPs). Each number should be a valid E.164 formatted phone number. Maximum of 10 numbers are allowed.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, array $numbers, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, array $numbers, Response $response, Database $dbForPlatform) { $uniqueNumbers = []; foreach ($numbers as $number) { @@ -930,7 +962,7 @@ App::patch('/v1/projects/:projectId/auth/mock-numbers') $uniqueNumbers[$number['phone']] = $number['otp']; } - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -940,7 +972,7 @@ App::patch('/v1/projects/:projectId/auth/mock-numbers') $auths['mockNumbers'] = $numbers; - $project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('auths', $auths)); + $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('auths', $auths)); $response->dynamic($project, Response::MODEL_PROJECT); }); @@ -958,10 +990,10 @@ App::delete('/v1/projects/:projectId') ->param('projectId', '', new UID(), 'Project unique ID.') ->inject('response') ->inject('user') - ->inject('dbForConsole') + ->inject('dbForPlatform') ->inject('queueForDeletes') - ->action(function (string $projectId, Response $response, Document $user, Database $dbForConsole, Delete $queueForDeletes) { - $project = $dbForConsole->getDocument('projects', $projectId); + ->action(function (string $projectId, Response $response, Document $user, Database $dbForPlatform, Delete $queueForDeletes) { + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -972,7 +1004,7 @@ App::delete('/v1/projects/:projectId') ->setType(DELETE_TYPE_DOCUMENT) ->setDocument($project); - if (!$dbForConsole->deleteDocument('projects', $projectId)) { + if (!$dbForPlatform->deleteDocument('projects', $projectId)) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove project from DB'); } @@ -1000,10 +1032,10 @@ App::post('/v1/projects/:projectId/webhooks') ->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true) ->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true) ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -1030,9 +1062,9 @@ App::post('/v1/projects/:projectId/webhooks') 'enabled' => $enabled, ]); - $webhook = $dbForConsole->createDocument('webhooks', $webhook); + $webhook = $dbForPlatform->createDocument('webhooks', $webhook); - $dbForConsole->purgeCachedDocument('projects', $project->getId()); + $dbForPlatform->purgeCachedDocument('projects', $project->getId()); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -1051,16 +1083,16 @@ App::get('/v1/projects/:projectId/webhooks') ->label('sdk.response.model', Response::MODEL_WEBHOOK_LIST) ->param('projectId', '', new UID(), 'Project unique ID.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - $webhooks = $dbForConsole->find('webhooks', [ + $webhooks = $dbForPlatform->find('webhooks', [ Query::equal('projectInternalId', [$project->getInternalId()]), Query::limit(5000), ]); @@ -1084,16 +1116,16 @@ App::get('/v1/projects/:projectId/webhooks/:webhookId') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('webhookId', '', new UID(), 'Webhook unique ID.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - $webhook = $dbForConsole->findOne('webhooks', [ + $webhook = $dbForPlatform->findOne('webhooks', [ Query::equal('$id', [$webhookId]), Query::equal('projectInternalId', [$project->getInternalId()]), ]); @@ -1125,10 +1157,10 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId') ->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true) ->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true) ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $webhookId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $webhookId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -1136,7 +1168,7 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId') $security = ($security === '1' || $security === 'true' || $security === 1 || $security === true); - $webhook = $dbForConsole->findOne('webhooks', [ + $webhook = $dbForPlatform->findOne('webhooks', [ Query::equal('$id', [$webhookId]), Query::equal('projectInternalId', [$project->getInternalId()]), ]); @@ -1158,8 +1190,8 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId') $webhook->setAttribute('attempts', 0); } - $dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook); - $dbForConsole->purgeCachedDocument('projects', $project->getId()); + $dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook); + $dbForPlatform->purgeCachedDocument('projects', $project->getId()); $response->dynamic($webhook, Response::MODEL_WEBHOOK); }); @@ -1177,16 +1209,16 @@ App::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('webhookId', '', new UID(), 'Webhook unique ID.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - $webhook = $dbForConsole->findOne('webhooks', [ + $webhook = $dbForPlatform->findOne('webhooks', [ Query::equal('$id', [$webhookId]), Query::equal('projectInternalId', [$project->getInternalId()]), ]); @@ -1197,8 +1229,8 @@ App::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') $webhook->setAttribute('signatureKey', \bin2hex(\random_bytes(64))); - $dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook); - $dbForConsole->purgeCachedDocument('projects', $project->getId()); + $dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook); + $dbForPlatform->purgeCachedDocument('projects', $project->getId()); $response->dynamic($webhook, Response::MODEL_WEBHOOK); }); @@ -1215,16 +1247,16 @@ App::delete('/v1/projects/:projectId/webhooks/:webhookId') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('webhookId', '', new UID(), 'Webhook unique ID.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - $webhook = $dbForConsole->findOne('webhooks', [ + $webhook = $dbForPlatform->findOne('webhooks', [ Query::equal('$id', [$webhookId]), Query::equal('projectInternalId', [$project->getInternalId()]), ]); @@ -1233,9 +1265,9 @@ App::delete('/v1/projects/:projectId/webhooks/:webhookId') throw new Exception(Exception::WEBHOOK_NOT_FOUND); } - $dbForConsole->deleteDocument('webhooks', $webhook->getId()); + $dbForPlatform->deleteDocument('webhooks', $webhook->getId()); - $dbForConsole->purgeCachedDocument('projects', $project->getId()); + $dbForPlatform->purgeCachedDocument('projects', $project->getId()); $response->noContent(); }); @@ -1257,10 +1289,10 @@ App::post('/v1/projects/:projectId/keys') ->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.') ->param('expire', null, new DatetimeValidator(), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true) ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -1283,9 +1315,9 @@ App::post('/v1/projects/:projectId/keys') 'secret' => API_KEY_STANDARD . '_' . \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) @@ -1304,16 +1336,16 @@ App::get('/v1/projects/:projectId/keys') ->label('sdk.response.model', Response::MODEL_KEY_LIST) ->param('projectId', '', new UID(), 'Project unique ID.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - $keys = $dbForConsole->find('keys', [ + $keys = $dbForPlatform->find('keys', [ Query::equal('projectInternalId', [$project->getInternalId()]), Query::limit(5000), ]); @@ -1337,16 +1369,16 @@ App::get('/v1/projects/:projectId/keys/:keyId') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('keyId', '', new UID(), 'Key unique ID.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $keyId, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $keyId, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - $key = $dbForConsole->findOne('keys', [ + $key = $dbForPlatform->findOne('keys', [ Query::equal('$id', [$keyId]), Query::equal('projectInternalId', [$project->getInternalId()]), ]); @@ -1374,16 +1406,16 @@ App::put('/v1/projects/:projectId/keys/:keyId') ->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.') ->param('expire', null, new DatetimeValidator(), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true) ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - $key = $dbForConsole->findOne('keys', [ + $key = $dbForPlatform->findOne('keys', [ Query::equal('$id', [$keyId]), Query::equal('projectInternalId', [$project->getInternalId()]), ]); @@ -1397,9 +1429,9 @@ App::put('/v1/projects/:projectId/keys/:keyId') ->setAttribute('scopes', $scopes) ->setAttribute('expire', $expire); - $dbForConsole->updateDocument('keys', $key->getId(), $key); + $dbForPlatform->updateDocument('keys', $key->getId(), $key); - $dbForConsole->purgeCachedDocument('projects', $project->getId()); + $dbForPlatform->purgeCachedDocument('projects', $project->getId()); $response->dynamic($key, Response::MODEL_KEY); }); @@ -1416,16 +1448,16 @@ App::delete('/v1/projects/:projectId/keys/:keyId') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('keyId', '', new UID(), 'Key unique ID.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $keyId, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $keyId, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - $key = $dbForConsole->findOne('keys', [ + $key = $dbForPlatform->findOne('keys', [ Query::equal('$id', [$keyId]), Query::equal('projectInternalId', [$project->getInternalId()]), ]); @@ -1434,9 +1466,9 @@ App::delete('/v1/projects/:projectId/keys/:keyId') throw new Exception(Exception::KEY_NOT_FOUND); } - $dbForConsole->deleteDocument('keys', $key->getId()); + $dbForPlatform->deleteDocument('keys', $key->getId()); - $dbForConsole->purgeCachedDocument('projects', $project->getId()); + $dbForPlatform->purgeCachedDocument('projects', $project->getId()); $response->noContent(); }); @@ -1457,10 +1489,10 @@ App::post('/v1/projects/:projectId/jwts') ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for JWT key. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.') ->param('duration', 900, new Range(0, 3600), 'Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds.', true) ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, array $scopes, int $duration, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, array $scopes, int $duration, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -1496,9 +1528,9 @@ App::post('/v1/projects/:projectId/platforms') ->param('store', '', new Text(256), 'App store or Google Play store ID. Max length: 256 chars.', true) ->param('hostname', '', new Hostname(), 'Platform client hostname. Max length: 256 chars.', true) ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $type, string $name, string $key, string $store, string $hostname, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); + ->inject('dbForPlatform') + ->action(function (string $projectId, string $type, string $name, string $key, string $store, string $hostname, Response $response, Database $dbForPlatform) { + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -1520,9 +1552,9 @@ App::post('/v1/projects/:projectId/platforms') 'hostname' => $hostname ]); - $platform = $dbForConsole->createDocument('platforms', $platform); + $platform = $dbForPlatform->createDocument('platforms', $platform); - $dbForConsole->purgeCachedDocument('projects', $project->getId()); + $dbForPlatform->purgeCachedDocument('projects', $project->getId()); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -1541,16 +1573,16 @@ App::get('/v1/projects/:projectId/platforms') ->label('sdk.response.model', Response::MODEL_PLATFORM_LIST) ->param('projectId', '', new UID(), 'Project unique ID.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - $platforms = $dbForConsole->find('platforms', [ + $platforms = $dbForPlatform->find('platforms', [ Query::equal('projectInternalId', [$project->getInternalId()]), Query::limit(5000), ]); @@ -1574,16 +1606,16 @@ App::get('/v1/projects/:projectId/platforms/:platformId') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('platformId', '', new UID(), 'Platform unique ID.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $platformId, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $platformId, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - $platform = $dbForConsole->findOne('platforms', [ + $platform = $dbForPlatform->findOne('platforms', [ Query::equal('$id', [$platformId]), Query::equal('projectInternalId', [$project->getInternalId()]), ]); @@ -1612,15 +1644,15 @@ App::put('/v1/projects/:projectId/platforms/:platformId') ->param('store', '', new Text(256), 'App store or Google Play store ID. Max length: 256 chars.', true) ->param('hostname', '', new Hostname(), 'Platform client URL. Max length: 256 chars.', true) ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $platformId, string $name, string $key, string $store, string $hostname, Response $response, Database $dbForConsole) { - $project = $dbForConsole->getDocument('projects', $projectId); + ->inject('dbForPlatform') + ->action(function (string $projectId, string $platformId, string $name, string $key, string $store, string $hostname, Response $response, Database $dbForPlatform) { + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - $platform = $dbForConsole->findOne('platforms', [ + $platform = $dbForPlatform->findOne('platforms', [ Query::equal('$id', [$platformId]), Query::equal('projectInternalId', [$project->getInternalId()]), ]); @@ -1635,9 +1667,9 @@ App::put('/v1/projects/:projectId/platforms/:platformId') ->setAttribute('store', $store) ->setAttribute('hostname', $hostname); - $dbForConsole->updateDocument('platforms', $platform->getId(), $platform); + $dbForPlatform->updateDocument('platforms', $platform->getId(), $platform); - $dbForConsole->purgeCachedDocument('projects', $project->getId()); + $dbForPlatform->purgeCachedDocument('projects', $project->getId()); $response->dynamic($platform, Response::MODEL_PLATFORM); }); @@ -1655,16 +1687,16 @@ App::delete('/v1/projects/:projectId/platforms/:platformId') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('platformId', '', new UID(), 'Platform unique ID.') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $platformId, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $platformId, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - $platform = $dbForConsole->findOne('platforms', [ + $platform = $dbForPlatform->findOne('platforms', [ Query::equal('$id', [$platformId]), Query::equal('projectInternalId', [$project->getInternalId()]), ]); @@ -1673,9 +1705,9 @@ App::delete('/v1/projects/:projectId/platforms/:platformId') throw new Exception(Exception::PLATFORM_NOT_FOUND); } - $dbForConsole->deleteDocument('platforms', $platformId); + $dbForPlatform->deleteDocument('platforms', $platformId); - $dbForConsole->purgeCachedDocument('projects', $project->getId()); + $dbForPlatform->purgeCachedDocument('projects', $project->getId()); $response->noContent(); }); @@ -1703,10 +1735,10 @@ App::patch('/v1/projects/:projectId/smtp') ->param('password', '', new Text(0, 0), 'SMTP server password', true) ->param('secure', '', new WhiteList(['tls', 'ssl'], true), 'Does SMTP server use secure connection', true) ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, bool $enabled, string $senderName, string $senderEmail, string $replyTo, string $host, int $port, string $username, string $password, string $secure, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, bool $enabled, string $senderName, string $senderEmail, string $replyTo, string $host, int $port, string $username, string $password, string $secure, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -1767,7 +1799,7 @@ App::patch('/v1/projects/:projectId/smtp') ]; } - $project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('smtp', $smtp)); + $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('smtp', $smtp)); $response->dynamic($project, Response::MODEL_PROJECT); }); @@ -1792,10 +1824,10 @@ App::post('/v1/projects/:projectId/smtp/tests') ->param('password', '', new Text(0, 0), 'SMTP server password', true) ->param('secure', '', new WhiteList(['tls', 'ssl'], true), 'Does SMTP server use secure connection', true) ->inject('response') - ->inject('dbForConsole') + ->inject('dbForPlatform') ->inject('queueForMails') - ->action(function (string $projectId, array $emails, string $senderName, string $senderEmail, string $replyTo, string $host, int $port, string $username, string $password, string $secure, Response $response, Database $dbForConsole, Mail $queueForMails) { - $project = $dbForConsole->getDocument('projects', $projectId); + ->action(function (string $projectId, array $emails, string $senderName, string $senderEmail, string $replyTo, string $host, int $port, string $username, string $password, string $secure, Response $response, Database $dbForPlatform, Mail $queueForMails) { + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -1845,12 +1877,12 @@ App::get('/v1/projects/:projectId/templates/sms/:type/:locale') ->param('type', '', new WhiteList(Config::getParam('locale-templates')['sms'] ?? []), 'Template type') ->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes']) ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform) { 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); @@ -1886,10 +1918,10 @@ App::get('/v1/projects/:projectId/templates/email/:type/:locale') ->param('type', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? []), 'Template type') ->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes']) ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -1939,12 +1971,12 @@ App::patch('/v1/projects/:projectId/templates/sms/:type/:locale') ->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes']) ->param('message', '', new Text(0), 'Template message') ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $type, string $locale, string $message, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $type, string $locale, string $message, Response $response, Database $dbForPlatform) { 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); @@ -1955,7 +1987,7 @@ App::patch('/v1/projects/:projectId/templates/sms/:type/:locale') 'message' => $message ]; - $project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates)); + $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates)); $response->dynamic(new Document([ 'message' => $message, @@ -1983,10 +2015,10 @@ App::patch('/v1/projects/:projectId/templates/email/:type/:locale') ->param('senderEmail', '', new Email(), 'Email of the sender', true) ->param('replyTo', '', new Email(), 'Reply to email', true) ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $type, string $locale, string $subject, string $message, string $senderName, string $senderEmail, string $replyTo, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $type, string $locale, string $subject, string $message, string $senderName, string $senderEmail, string $replyTo, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -2001,7 +2033,7 @@ App::patch('/v1/projects/:projectId/templates/email/:type/:locale') 'message' => $message ]; - $project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates)); + $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates)); $response->dynamic(new Document([ 'type' => $type, @@ -2028,12 +2060,12 @@ App::delete('/v1/projects/:projectId/templates/sms/:type/:locale') ->param('type', '', new WhiteList(Config::getParam('locale-templates')['sms'] ?? []), 'Template type') ->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes']) ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform) { 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); @@ -2048,7 +2080,7 @@ App::delete('/v1/projects/:projectId/templates/sms/:type/:locale') unset($template['sms.' . $type . '-' . $locale]); - $project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates)); + $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates)); $response->dynamic(new Document([ 'type' => $type, @@ -2071,10 +2103,10 @@ App::delete('/v1/projects/:projectId/templates/email/:type/:locale') ->param('type', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? []), 'Template type') ->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes']) ->inject('response') - ->inject('dbForConsole') - ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) { + ->inject('dbForPlatform') + ->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForPlatform) { - $project = $dbForConsole->getDocument('projects', $projectId); + $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); @@ -2089,7 +2121,7 @@ App::delete('/v1/projects/:projectId/templates/email/:type/:locale') unset($templates['email.' . $type . '-' . $locale]); - $project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates)); + $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates)); $response->dynamic(new Document([ 'type' => $type, diff --git a/app/controllers/api/proxy.php b/app/controllers/api/proxy.php index 56fd31e88c..0d2fed8e66 100644 --- a/app/controllers/api/proxy.php +++ b/app/controllers/api/proxy.php @@ -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); diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 911481a1c0..e92b55f140 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -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) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index a7824c71e7..08f698095c 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -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 ); }); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 575349e827..7923d314ae 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -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 diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index bbb1d9c3f8..6e81c43ef8 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -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(); }); diff --git a/app/controllers/general.php b/app/controllers/general.php index 9c2d0076f3..54b29fce59 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -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 diff --git a/app/controllers/mock.php b/app/controllers/mock.php index bc071fc885..afbb176b7e 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -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([ diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index f5921bf6e8..012dd13c73 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -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() diff --git a/app/http.php b/app/http.php index b9e3552efd..3a7562ffd1 100644 --- a/app/http.php +++ b/app/http.php @@ -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(); diff --git a/app/init.php b/app/init.php index 7f9e1cdcb1..999387f67f 100644 --- a/app/init.php +++ b/app/init.php @@ -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']); diff --git a/app/realtime.php b/app/realtime.php index d38192b83c..86f9c85fdd 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -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); diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 8d7fecb479..ad6d883c4c 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -167,7 +167,7 @@ $image = $this->getParam('image', ''); appwrite-console: <<: *x-logging container_name: appwrite-console - image: /console:5.0.12 + image: /console:5.2.27 restart: unless-stopped networks: - appwrite diff --git a/app/worker.php b/app/worker.php index 4741afe7ea..6eb1363e9b 100644 --- a/app/worker.php +++ b/app/worker.php @@ -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()); diff --git a/composer.json b/composer.json index a04ca51d43..e44dd0bffc 100644 --- a/composer.json +++ b/composer.json @@ -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 } } } diff --git a/composer.lock b/composer.lock index 691a7e740e..b906af3a81 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b358198535c1867eabed7c0f99135a57", + "content-hash": "6b136b5490c0d5d331eac0d70bb3e198", "packages": [ { "name": "adhocore/jwt", @@ -157,16 +157,16 @@ }, { "name": "appwrite/php-runtimes", - "version": "0.16.4", + "version": "0.16.5", "source": { "type": "git", "url": "https://github.com/appwrite/runtimes.git", - "reference": "7e4741337b9373f77210396e68eca539018cabd1" + "reference": "1e430646fdf847a7caf3c611dcf3d6d5a28c3fd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/runtimes/zipball/7e4741337b9373f77210396e68eca539018cabd1", - "reference": "7e4741337b9373f77210396e68eca539018cabd1", + "url": "https://api.github.com/repos/appwrite/runtimes/zipball/1e430646fdf847a7caf3c611dcf3d6d5a28c3fd9", + "reference": "1e430646fdf847a7caf3c611dcf3d6d5a28c3fd9", "shasum": "" }, "require": { @@ -206,9 +206,9 @@ ], "support": { "issues": "https://github.com/appwrite/runtimes/issues", - "source": "https://github.com/appwrite/runtimes/tree/0.16.4" + "source": "https://github.com/appwrite/runtimes/tree/0.16.5" }, - "time": "2024-10-26T10:39:59+00:00" + "time": "2024-11-25T15:17:06+00:00" }, { "name": "beberlei/assert", @@ -277,6 +277,66 @@ }, "time": "2024-07-15T13:18:35+00:00" }, + { + "name": "brick/math", + "version": "0.12.1", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "5.16.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.12.1" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2023-11-29T23:19:16+00:00" + }, { "name": "chillerlan/php-qrcode", "version": "4.3.4", @@ -422,6 +482,87 @@ ], "time": "2024-07-17T01:04:28+00:00" }, + { + "name": "composer/semver", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-09-19T14:15:21+00:00" + }, { "name": "dragonmantank/cron-expression", "version": "v3.3.2", @@ -567,29 +708,73 @@ "time": "2024-05-03T06:31:11+00:00" }, { - "name": "jean85/pretty-package-versions", - "version": "2.0.6", + "name": "google/protobuf", + "version": "v4.29.2", "source": { "type": "git", - "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4" + "url": "https://github.com/protocolbuffers/protobuf-php.git", + "reference": "79aa5014efeeec3d137df5cdb0ae2fc163953945" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/f9fdd29ad8e6d024f52678b570e5593759b550b4", - "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/79aa5014efeeec3d137df5cdb0ae2fc163953945", + "reference": "79aa5014efeeec3d137df5cdb0ae2fc163953945", "shasum": "" }, "require": { - "composer-runtime-api": "^2.0.0", - "php": "^7.1|^8.0" + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": ">=5.0.0" + }, + "suggest": { + "ext-bcmath": "Need to support JSON deserialization" + }, + "type": "library", + "autoload": { + "psr-4": { + "Google\\Protobuf\\": "src/Google/Protobuf", + "GPBMetadata\\Google\\Protobuf\\": "src/GPBMetadata/Google/Protobuf" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "proto library for PHP", + "homepage": "https://developers.google.com/protocol-buffers/", + "keywords": [ + "proto" + ], + "support": { + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.29.2" + }, + "time": "2024-12-18T14:11:12+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/3c4e5f62ba8d7de1734312e4fff32f67a8daaf10", + "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.2", "jean85/composer-provided-replaced-stub-package": "^1.0", "phpstan/phpstan": "^1.4", - "phpunit/phpunit": "^7.5|^8.5|^9.4", - "vimeo/psalm": "^4.3" + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "vimeo/psalm": "^4.3 || ^5.0" }, "type": "library", "extra": { @@ -621,9 +806,9 @@ ], "support": { "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.6" + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.0" }, - "time": "2024-03-08T09:58:59+00:00" + "time": "2024-11-18T16:19:46+00:00" }, { "name": "league/csv", @@ -906,6 +1091,553 @@ }, "time": "2019-09-10T13:16:29+00:00" }, + { + "name": "nyholm/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-09-09T07:06:30+00:00" + }, + { + "name": "nyholm/psr7-server", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7-server.git", + "reference": "4335801d851f554ca43fa6e7d2602141538854dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7-server/zipball/4335801d851f554ca43fa6e7d2602141538854dc", + "reference": "4335801d851f554ca43fa6e7d2602141538854dc", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "nyholm/nsa": "^1.1", + "nyholm/psr7": "^1.3", + "phpunit/phpunit": "^7.0 || ^8.5 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Nyholm\\Psr7Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "Helper classes to handle PSR-7 server requests", + "homepage": "http://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7-server/issues", + "source": "https://github.com/Nyholm/psr7-server/tree/1.1.0" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2023-11-08T09:30:43+00:00" + }, + { + "name": "open-telemetry/api", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/api.git", + "reference": "04c85a1e41a3d59fa9bdc801a5de1df6624b95ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/04c85a1e41a3d59fa9bdc801a5de1df6624b95ed", + "reference": "04c85a1e41a3d59fa9bdc801a5de1df6624b95ed", + "shasum": "" + }, + "require": { + "open-telemetry/context": "^1.0", + "php": "^8.1", + "psr/log": "^1.1|^2.0|^3.0", + "symfony/polyfill-php82": "^1.26" + }, + "conflict": { + "open-telemetry/sdk": "<=1.0.8" + }, + "type": "library", + "extra": { + "spi": { + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" + ] + }, + "branch-alias": { + "dev-main": "1.1.x-dev" + } + }, + "autoload": { + "files": [ + "Trace/functions.php" + ], + "psr-4": { + "OpenTelemetry\\API\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "API for OpenTelemetry PHP.", + "keywords": [ + "Metrics", + "api", + "apm", + "logging", + "opentelemetry", + "otel", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2024-11-16T04:32:30+00:00" + }, + { + "name": "open-telemetry/context", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/context.git", + "reference": "0cba875ea1953435f78aec7f1d75afa87bdbf7f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/0cba875ea1953435f78aec7f1d75afa87bdbf7f3", + "reference": "0cba875ea1953435f78aec7f1d75afa87bdbf7f3", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/polyfill-php82": "^1.26" + }, + "suggest": { + "ext-ffi": "To allow context switching in Fibers" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "files": [ + "fiber/initialize_fiber_handler.php" + ], + "psr-4": { + "OpenTelemetry\\Context\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "Context implementation for OpenTelemetry PHP.", + "keywords": [ + "Context", + "opentelemetry", + "otel" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2024-08-21T00:29:20+00:00" + }, + { + "name": "open-telemetry/exporter-otlp", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/exporter-otlp.git", + "reference": "9b6de12204f25f8ab9540b46d6e7b5151897ce18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/9b6de12204f25f8ab9540b46d6e7b5151897ce18", + "reference": "9b6de12204f25f8ab9540b46d6e7b5151897ce18", + "shasum": "" + }, + "require": { + "open-telemetry/api": "^1.0", + "open-telemetry/gen-otlp-protobuf": "^1.1", + "open-telemetry/sdk": "^1.0", + "php": "^8.1", + "php-http/discovery": "^1.14" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "files": [ + "_register.php" + ], + "psr-4": { + "OpenTelemetry\\Contrib\\Otlp\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "OTLP exporter for OpenTelemetry.", + "keywords": [ + "Metrics", + "exporter", + "gRPC", + "http", + "opentelemetry", + "otel", + "otlp", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2024-04-30T18:28:30+00:00" + }, + { + "name": "open-telemetry/gen-otlp-protobuf", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/gen-otlp-protobuf.git", + "reference": "66c3b98e998a726691c92e6405a82e6e7b8b169d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/66c3b98e998a726691c92e6405a82e6e7b8b169d", + "reference": "66c3b98e998a726691c92e6405a82e6e7b8b169d", + "shasum": "" + }, + "require": { + "google/protobuf": "^3.22 || ^4.0", + "php": "^8.0" + }, + "suggest": { + "ext-protobuf": "For better performance, when dealing with the protobuf format" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opentelemetry\\Proto\\": "Opentelemetry/Proto/", + "GPBMetadata\\Opentelemetry\\": "GPBMetadata/Opentelemetry/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "PHP protobuf files for communication with OpenTelemetry OTLP collectors/servers.", + "keywords": [ + "Metrics", + "apm", + "gRPC", + "logging", + "opentelemetry", + "otel", + "otlp", + "protobuf", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2024-10-30T11:49:49+00:00" + }, + { + "name": "open-telemetry/sdk", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/sdk.git", + "reference": "fb0ff8d8279a3776bd604791e2531dd0cc147e8b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/fb0ff8d8279a3776bd604791e2531dd0cc147e8b", + "reference": "fb0ff8d8279a3776bd604791e2531dd0cc147e8b", + "shasum": "" + }, + "require": { + "ext-json": "*", + "nyholm/psr7-server": "^1.1", + "open-telemetry/api": "~1.0 || ~1.1", + "open-telemetry/context": "^1.0", + "open-telemetry/sem-conv": "^1.0", + "php": "^8.1", + "php-http/discovery": "^1.14", + "psr/http-client": "^1.0", + "psr/http-client-implementation": "^1.0", + "psr/http-factory-implementation": "^1.0", + "psr/http-message": "^1.0.1|^2.0", + "psr/log": "^1.1|^2.0|^3.0", + "ramsey/uuid": "^3.0 || ^4.0", + "symfony/polyfill-mbstring": "^1.23", + "symfony/polyfill-php82": "^1.26", + "tbachert/spi": "^1.0.1" + }, + "suggest": { + "ext-gmp": "To support unlimited number of synchronous metric readers", + "ext-mbstring": "To increase performance of string operations", + "open-telemetry/sdk-configuration": "File-based OpenTelemetry SDK configuration" + }, + "type": "library", + "extra": { + "spi": { + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" + ] + }, + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "files": [ + "Common/Util/functions.php", + "Logs/Exporter/_register.php", + "Metrics/MetricExporter/_register.php", + "Propagation/_register.php", + "Trace/SpanExporter/_register.php", + "Common/Dev/Compatibility/_load.php", + "_autoload.php" + ], + "psr-4": { + "OpenTelemetry\\SDK\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "SDK for OpenTelemetry PHP.", + "keywords": [ + "Metrics", + "apm", + "logging", + "opentelemetry", + "otel", + "sdk", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2024-10-18T21:01:35+00:00" + }, + { + "name": "open-telemetry/sem-conv", + "version": "1.27.1", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/sem-conv.git", + "reference": "1dba705fea74bc0718d04be26090e3697e56f4e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/1dba705fea74bc0718d04be26090e3697e56f4e6", + "reference": "1dba705fea74bc0718d04be26090e3697e56f4e6", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "OpenTelemetry\\SemConv\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "description": "Semantic conventions for OpenTelemetry PHP.", + "keywords": [ + "Metrics", + "apm", + "logging", + "opentelemetry", + "otel", + "semantic conventions", + "semconv", + "tracing" + ], + "support": { + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", + "docs": "https://opentelemetry.io/docs/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2024-08-28T09:20:31+00:00" + }, { "name": "paragonie/constant_time_encoding", "version": "v2.7.0", @@ -973,6 +1705,85 @@ }, "time": "2024-05-08T12:18:48+00:00" }, + { + "name": "php-http/discovery", + "version": "1.20.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.20.0" + }, + "time": "2024-10-02T11:20:13+00:00" + }, { "name": "phpmailer/phpmailer", "version": "v6.9.1", @@ -1054,6 +1865,450 @@ ], "time": "2023-11-25T22:23:28+00:00" }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.28.3", + "fakerphp/faker": "^1.21", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^1.0", + "mockery/mockery": "^1.5", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpcsstandards/phpcsutils": "^1.0.0-rc1", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.18.4", + "ramsey/coding-standard": "^2.0.3", + "ramsey/conventional-commits": "^1.3", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", + "type": "tidelift" + } + ], + "time": "2022-12-31T21:50:55+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.7.6", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", + "ext-json": "*", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.10", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "doctrine/annotations": "^1.8", + "ergebnis/composer-normalize": "^2.15", + "mockery/mockery": "^1.3", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.2", + "php-mock/php-mock-mockery": "^1.3", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^8.5 || ^9", + "ramsey/composer-repl": "^1.4", + "slevomat/coding-standard": "^8.4", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.9" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.7.6" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", + "type": "tidelift" + } + ], + "time": "2024-04-27T21:32:50+00:00" + }, { "name": "spomky-labs/otphp", "version": "v10.0.3", @@ -1088,9 +2343,9 @@ "type": "library", "extra": { "branch-alias": { - "v10.0": "10.0.x-dev", + "v8.3": "8.3.x-dev", "v9.0": "9.0.x-dev", - "v8.3": "8.3.x-dev" + "v10.0": "10.0.x-dev" } }, "autoload": { @@ -1129,6 +2384,246 @@ }, "time": "2022-03-17T08:00:35+00:00" }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/http-client", + "version": "v7.2.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "339ba21476eb184290361542f732ad12c97591ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/339ba21476eb184290361542f732ad12c97591ec", + "reference": "339ba21476eb184290361542f732ad12c97591ec", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "amphp/amp": "<2.5", + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/amphp-http-client-meta": "^1.0|^2.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v7.2.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-30T18:35:15+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.5.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-07T08:49:48+00:00" + }, { "name": "symfony/polyfill-mbstring", "version": "v1.31.0", @@ -1155,8 +2650,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1229,8 +2724,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1289,6 +2784,217 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-php82", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php82.git", + "reference": "5d2ed36f7734637dacc025f179698031951b1692" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/5d2ed36f7734637dacc025f179698031951b1692", + "reference": "5d2ed36f7734637dacc025f179698031951b1692", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php82\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php82/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "tbachert/spi", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/Nevay/spi.git", + "reference": "2ddfaf815dafb45791a61b08170de8d583c16062" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nevay/spi/zipball/2ddfaf815dafb45791a61b08170de8d583c16062", + "reference": "2ddfaf815dafb45791a61b08170de8d583c16062", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "composer/semver": "^1.0 || ^2.0 || ^3.0", + "php": "^8.1" + }, + "require-dev": { + "composer/composer": "^2.0", + "infection/infection": "^0.27.9", + "phpunit/phpunit": "^10.5", + "psalm/phar": "^5.18" + }, + "type": "composer-plugin", + "extra": { + "class": "Nevay\\SPI\\Composer\\Plugin", + "branch-alias": { + "dev-main": "0.2.x-dev" + }, + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Nevay\\SPI\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "Service provider loading facility", + "keywords": [ + "service provider" + ], + "support": { + "issues": "https://github.com/Nevay/spi/issues", + "source": "https://github.com/Nevay/spi/tree/v1.0.2" + }, + "time": "2024-10-04T16:36:12+00:00" + }, { "name": "thecodingmachine/safe", "version": "v2.5.0", @@ -1430,16 +3136,16 @@ }, { "name": "utopia-php/abuse", - "version": "0.43.0", + "version": "0.46.0", "source": { "type": "git", "url": "https://github.com/utopia-php/abuse.git", - "reference": "6346a3b4c5177a43160035a7289e30fdfb0790d6" + "reference": "bef426a9a28b3e71b08216bcbe310455f588c11b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/abuse/zipball/6346a3b4c5177a43160035a7289e30fdfb0790d6", - "reference": "6346a3b4c5177a43160035a7289e30fdfb0790d6", + "url": "https://api.github.com/repos/utopia-php/abuse/zipball/bef426a9a28b3e71b08216bcbe310455f588c11b", + "reference": "bef426a9a28b3e71b08216bcbe310455f588c11b", "shasum": "" }, "require": { @@ -1447,7 +3153,7 @@ "ext-pdo": "*", "ext-redis": "*", "php": ">=8.0", - "utopia-php/database": "0.53.*" + "utopia-php/database": "0.53.200" }, "require-dev": { "laravel/pint": "1.5.*", @@ -1475,9 +3181,9 @@ ], "support": { "issues": "https://github.com/utopia-php/abuse/issues", - "source": "https://github.com/utopia-php/abuse/tree/0.43.0" + "source": "https://github.com/utopia-php/abuse/tree/0.46.0" }, - "time": "2024-08-30T05:17:23+00:00" + "time": "2024-12-27T17:46:08+00:00" }, { "name": "utopia-php/analytics", @@ -1527,16 +3233,16 @@ }, { "name": "utopia-php/audit", - "version": "0.43.0", + "version": "0.43.1", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "cef22b5dc6a6d28fcd522f41c7bf7ded4a4dfd3e" + "reference": "04a47dd1f5f92e2d50e971a06bcc9e759325d277" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/cef22b5dc6a6d28fcd522f41c7bf7ded4a4dfd3e", - "reference": "cef22b5dc6a6d28fcd522f41c7bf7ded4a4dfd3e", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/04a47dd1f5f92e2d50e971a06bcc9e759325d277", + "reference": "04a47dd1f5f92e2d50e971a06bcc9e759325d277", "shasum": "" }, "require": { @@ -1568,9 +3274,9 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/0.43.0" + "source": "https://github.com/utopia-php/audit/tree/0.43.1" }, - "time": "2024-08-30T05:17:36+00:00" + "time": "2024-10-23T04:27:59+00:00" }, { "name": "utopia-php/cache", @@ -1770,16 +3476,16 @@ }, { "name": "utopia-php/database", - "version": "0.53.16", + "version": "0.53.200", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "6661edffeef05b59e16d102b989a72f7f78cf7de" + "reference": "570c63a3760d0e1404679ddfacd9484af40bd9fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/6661edffeef05b59e16d102b989a72f7f78cf7de", - "reference": "6661edffeef05b59e16d102b989a72f7f78cf7de", + "url": "https://api.github.com/repos/utopia-php/database/zipball/570c63a3760d0e1404679ddfacd9484af40bd9fc", + "reference": "570c63a3760d0e1404679ddfacd9484af40bd9fc", "shasum": "" }, "require": { @@ -1820,9 +3526,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.53.16" + "source": "https://github.com/utopia-php/database/tree/0.53.200" }, - "time": "2024-11-06T03:07:16+00:00" + "time": "2024-12-01T07:59:15+00:00" }, { "name": "utopia-php/domains", @@ -1972,21 +3678,22 @@ }, { "name": "utopia-php/framework", - "version": "0.33.11", + "version": "0.33.15", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "354ff0d23bfc6e82bea0fe8e89e115cff1af8466" + "reference": "83b0628900c2c53e8c3efbf069f3e13050295edc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/354ff0d23bfc6e82bea0fe8e89e115cff1af8466", - "reference": "354ff0d23bfc6e82bea0fe8e89e115cff1af8466", + "url": "https://api.github.com/repos/utopia-php/http/zipball/83b0628900c2c53e8c3efbf069f3e13050295edc", + "reference": "83b0628900c2c53e8c3efbf069f3e13050295edc", "shasum": "" }, "require": { - "php": ">=8.0", - "utopia-php/compression": "0.1.*" + "php": ">=8.1", + "utopia-php/compression": "0.1.*", + "utopia-php/telemetry": "0.1.*" }, "require-dev": { "laravel/pint": "^1.2", @@ -2012,9 +3719,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.11" + "source": "https://github.com/utopia-php/http/tree/0.33.15" }, - "time": "2024-11-08T18:47:43+00:00" + "time": "2024-12-10T13:07:04+00:00" }, { "name": "utopia-php/image", @@ -2222,16 +3929,16 @@ }, { "name": "utopia-php/migration", - "version": "0.6.11", + "version": "0.6.13", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "4d167914d3f7fa1fe816b2b2c6f221e70166bfd7" + "reference": "68d9b0a9477755afcda607e7e8109785cae17a13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/4d167914d3f7fa1fe816b2b2c6f221e70166bfd7", - "reference": "4d167914d3f7fa1fe816b2b2c6f221e70166bfd7", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/68d9b0a9477755afcda607e7e8109785cae17a13", + "reference": "68d9b0a9477755afcda607e7e8109785cae17a13", "shasum": "" }, "require": { @@ -2272,9 +3979,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/0.6.11" + "source": "https://github.com/utopia-php/migration/tree/0.6.13" }, - "time": "2024-10-31T06:19:57+00:00" + "time": "2024-11-26T13:57:53+00:00" }, { "name": "utopia-php/mongo", @@ -2542,22 +4249,23 @@ }, { "name": "utopia-php/queue", - "version": "0.7.1", + "version": "0.7.3", "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "94c240d9f6383829807ce7b2d737f04b159fd3e8" + "reference": "16074a98ee7d6212bc1228de200e13db470c098a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/queue/zipball/94c240d9f6383829807ce7b2d737f04b159fd3e8", - "reference": "94c240d9f6383829807ce7b2d737f04b159fd3e8", + "url": "https://api.github.com/repos/utopia-php/queue/zipball/16074a98ee7d6212bc1228de200e13db470c098a", + "reference": "16074a98ee7d6212bc1228de200e13db470c098a", "shasum": "" }, "require": { - "php": ">=8.0", + "php": ">=8.1", "utopia-php/cli": "0.15.*", - "utopia-php/framework": "0.*.*" + "utopia-php/framework": "0.*.*", + "utopia-php/telemetry": "0.1.*" }, "require-dev": { "laravel/pint": "^0.2.3", @@ -2597,9 +4305,9 @@ ], "support": { "issues": "https://github.com/utopia-php/queue/issues", - "source": "https://github.com/utopia-php/queue/tree/0.7.1" + "source": "https://github.com/utopia-php/queue/tree/0.7.3" }, - "time": "2024-11-05T17:00:38+00:00" + "time": "2024-11-13T12:47:48+00:00" }, { "name": "utopia-php/registry", @@ -2655,16 +4363,16 @@ }, { "name": "utopia-php/storage", - "version": "0.18.6", + "version": "0.18.8", "source": { "type": "git", "url": "https://github.com/utopia-php/storage.git", - "reference": "893ccf06e183f8ece2aed8dbf14d64d6ba036071" + "reference": "84737afa634e6a833fc4f8b0c967553234d3f215" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/893ccf06e183f8ece2aed8dbf14d64d6ba036071", - "reference": "893ccf06e183f8ece2aed8dbf14d64d6ba036071", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/84737afa634e6a833fc4f8b0c967553234d3f215", + "reference": "84737afa634e6a833fc4f8b0c967553234d3f215", "shasum": "" }, "require": { @@ -2704,9 +4412,9 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/0.18.6" + "source": "https://github.com/utopia-php/storage/tree/0.18.8" }, - "time": "2024-11-06T09:58:50+00:00" + "time": "2024-12-04T08:30:35+00:00" }, { "name": "utopia-php/swoole", @@ -2816,17 +4524,67 @@ "time": "2024-10-09T14:44:01+00:00" }, { - "name": "utopia-php/vcs", - "version": "0.8.3", + "name": "utopia-php/telemetry", + "version": "0.1.0", "source": { "type": "git", - "url": "https://github.com/utopia-php/vcs.git", - "reference": "a032ed0611a8f4467aeaa9484f73223074457337" + "url": "https://github.com/utopia-php/telemetry.git", + "reference": "d35f2f0632f4ee0be63fb7ace6a94a6adda71a80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/vcs/zipball/a032ed0611a8f4467aeaa9484f73223074457337", - "reference": "a032ed0611a8f4467aeaa9484f73223074457337", + "url": "https://api.github.com/repos/utopia-php/telemetry/zipball/d35f2f0632f4ee0be63fb7ace6a94a6adda71a80", + "reference": "d35f2f0632f4ee0be63fb7ace6a94a6adda71a80", + "shasum": "" + }, + "require": { + "ext-opentelemetry": "*", + "ext-protobuf": "*", + "nyholm/psr7": "^1.8", + "open-telemetry/exporter-otlp": "^1.1", + "open-telemetry/sdk": "^1.1", + "php": ">=8.0", + "symfony/http-client": "^7.1" + }, + "require-dev": { + "laravel/pint": "^1.2", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.25" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "keywords": [ + "framework", + "php", + "upf" + ], + "support": { + "issues": "https://github.com/utopia-php/telemetry/issues", + "source": "https://github.com/utopia-php/telemetry/tree/0.1.0" + }, + "time": "2024-11-13T10:29:53+00:00" + }, + { + "name": "utopia-php/vcs", + "version": "0.8.6", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/vcs.git", + "reference": "b10225f54d5670f09f83e82e09de9d820ada6931" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/vcs/zipball/b10225f54d5670f09f83e82e09de9d820ada6931", + "reference": "b10225f54d5670f09f83e82e09de9d820ada6931", "shasum": "" }, "require": { @@ -2860,9 +4618,9 @@ ], "support": { "issues": "https://github.com/utopia-php/vcs/issues", - "source": "https://github.com/utopia-php/vcs/tree/0.8.3" + "source": "https://github.com/utopia-php/vcs/tree/0.8.6" }, - "time": "2024-11-05T17:10:09+00:00" + "time": "2024-12-10T13:13:23+00:00" }, { "name": "utopia-php/websocket", @@ -3049,16 +4807,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.39.24", + "version": "0.39.28", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "412451c87f6ef17e24e9a5cf41721043d74c60c8" + "reference": "6ff467858fe418e364460da905139216570a5d5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/412451c87f6ef17e24e9a5cf41721043d74c60c8", - "reference": "412451c87f6ef17e24e9a5cf41721043d74c60c8", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6ff467858fe418e364460da905139216570a5d5e", + "reference": "6ff467858fe418e364460da905139216570a5d5e", "shasum": "" }, "require": { @@ -3066,7 +4824,7 @@ "ext-json": "*", "ext-mbstring": "*", "matthiasmullie/minify": "1.3.*", - "php": ">=8.0", + "php": ">=8.3", "twig/twig": "3.14.*" }, "require-dev": { @@ -3094,9 +4852,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.39.24" + "source": "https://github.com/appwrite/sdk-generator/tree/0.39.28" }, - "time": "2024-10-09T19:13:27+00:00" + "time": "2024-12-30T11:17:25+00:00" }, { "name": "doctrine/annotations", @@ -3176,29 +4934,27 @@ }, { "name": "doctrine/deprecations", - "version": "1.1.3", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab" + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", - "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/31610dbb31faa98e6b5447b62340826f54fbc4e9", + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^9", - "phpstan/phpstan": "1.4.10 || 1.10.15", - "phpstan/phpstan-phpunit": "^1.0", + "doctrine/coding-standard": "^9 || ^12", + "phpstan/phpstan": "1.4.10 || 2.0.3", + "phpstan/phpstan-phpunit": "^1.0 || ^2", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "psalm/plugin-phpunit": "0.18.4", - "psr/log": "^1 || ^2 || ^3", - "vimeo/psalm": "4.30.0 || 5.12.0" + "psr/log": "^1 || ^2 || ^3" }, "suggest": { "psr/log": "Allows logging deprecations via PSR-3 logger implementation" @@ -3206,7 +4962,7 @@ "type": "library", "autoload": { "psr-4": { - "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + "Doctrine\\Deprecations\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -3217,9 +4973,9 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.3" + "source": "https://github.com/doctrine/deprecations/tree/1.1.4" }, - "time": "2024-01-30T19:34:25+00:00" + "time": "2024-12-07T21:18:45+00:00" }, { "name": "doctrine/instantiator", @@ -3370,16 +5126,16 @@ }, { "name": "laravel/pint", - "version": "v1.18.1", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "35c00c05ec43e6b46d295efc0f4386ceb30d50d9" + "reference": "8169513746e1bac70c85d6ea1524d9225d4886f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/35c00c05ec43e6b46d295efc0f4386ceb30d50d9", - "reference": "35c00c05ec43e6b46d295efc0f4386ceb30d50d9", + "url": "https://api.github.com/repos/laravel/pint/zipball/8169513746e1bac70c85d6ea1524d9225d4886f0", + "reference": "8169513746e1bac70c85d6ea1524d9225d4886f0", "shasum": "" }, "require": { @@ -3390,13 +5146,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.64.0", - "illuminate/view": "^10.48.20", - "larastan/larastan": "^2.9.8", - "laravel-zero/framework": "^10.4.0", + "friendsofphp/php-cs-fixer": "^3.66.0", + "illuminate/view": "^10.48.25", + "larastan/larastan": "^2.9.12", + "laravel-zero/framework": "^10.48.25", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.35.1" + "nunomaduro/termwind": "^1.17.0", + "pestphp/pest": "^2.36.0" }, "bin": [ "builds/pint" @@ -3432,7 +5188,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-09-24T17:22:50+00:00" + "time": "2024-12-30T16:20:10+00:00" }, { "name": "matthiasmullie/minify", @@ -3620,16 +5376,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.3.1", + "version": "v5.4.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", "shasum": "" }, "require": { @@ -3672,9 +5428,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" }, - "time": "2024-10-08T18:51:32+00:00" + "time": "2024-12-30T11:07:19+00:00" }, { "name": "phar-io/manifest", @@ -4051,16 +5807,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.5.1", + "version": "5.6.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "0c70d2c566e899666f367ab7b80986beb3581e6f" + "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/0c70d2c566e899666f367ab7b80986beb3581e6f", - "reference": "0c70d2c566e899666f367ab7b80986beb3581e6f", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", + "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", "shasum": "" }, "require": { @@ -4069,7 +5825,7 @@ "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.2", "phpdocumentor/type-resolver": "^1.7", - "phpstan/phpdoc-parser": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", "webmozart/assert": "^1.9.1" }, "require-dev": { @@ -4109,9 +5865,9 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.5.1" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.1" }, - "time": "2024-11-06T11:58:54+00:00" + "time": "2024-12-07T09:39:29+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -4173,26 +5929,27 @@ }, { "name": "phpspec/prophecy", - "version": "v1.19.0", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "67a759e7d8746d501c41536ba40cd9c0a07d6a87" + "reference": "a0165c648cab6a80311c74ffc708a07bb53ecc93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/67a759e7d8746d501c41536ba40cd9c0a07d6a87", - "reference": "67a759e7d8746d501c41536ba40cd9c0a07d6a87", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/a0165c648cab6a80311c74ffc708a07bb53ecc93", + "reference": "a0165c648cab6a80311c74ffc708a07bb53ecc93", "shasum": "" }, "require": { "doctrine/instantiator": "^1.2 || ^2.0", - "php": "^7.2 || 8.0.* || 8.1.* || 8.2.* || 8.3.*", + "php": "^7.2 || 8.0.* || 8.1.* || 8.2.* || 8.3.* || 8.4.*", "phpdocumentor/reflection-docblock": "^5.2", "sebastian/comparator": "^3.0 || ^4.0 || ^5.0 || ^6.0", "sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0 || ^6.0" }, "require-dev": { + "friendsofphp/php-cs-fixer": "^3.40", "phpspec/phpspec": "^6.0 || ^7.0", "phpstan/phpstan": "^1.9", "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0" @@ -4236,36 +5993,36 @@ ], "support": { "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.19.0" + "source": "https://github.com/phpspec/prophecy/tree/v1.20.0" }, - "time": "2024-02-29T11:52:51+00:00" + "time": "2024-11-19T13:12:41+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.33.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140" + "reference": "c00d78fb6b29658347f9d37ebe104bffadf36299" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/82a311fd3690fb2bf7b64d5c98f912b3dd746140", - "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/c00d78fb6b29658347f9d37ebe104bffadf36299", + "reference": "c00d78fb6b29658347f9d37ebe104bffadf36299", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { "doctrine/annotations": "^2.0", - "nikic/php-parser": "^4.15", + "nikic/php-parser": "^5.3.0", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.5", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", "symfony/process": "^5.2" }, "type": "library", @@ -4283,9 +6040,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.33.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.0.0" }, - "time": "2024-10-13T11:25:22+00:00" + "time": "2024-10-13T11:29:49+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4758,109 +6515,6 @@ }, "time": "2021-02-03T23:26:27+00:00" }, - { - "name": "psr/container", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" - }, - "time": "2021-11-05T16:47:00+00:00" - }, - { - "name": "psr/log", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "shasum": "" - }, - "require": { - "php": ">=8.0.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "support": { - "source": "https://github.com/php-fig/log/tree/3.0.2" - }, - "time": "2024-09-11T13:17:53+00:00" - }, { "name": "sebastian/cli-parser", "version": "1.0.2", @@ -5922,16 +7576,16 @@ }, { "name": "symfony/console", - "version": "v7.1.7", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "3284aafcac338b6e86fd955ee4d794cbe434151a" + "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/3284aafcac338b6e86fd955ee4d794cbe434151a", - "reference": "3284aafcac338b6e86fd955ee4d794cbe434151a", + "url": "https://api.github.com/repos/symfony/console/zipball/fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3", "shasum": "" }, "require": { @@ -5995,7 +7649,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.1.7" + "source": "https://github.com/symfony/console/tree/v7.2.1" }, "funding": [ { @@ -6011,87 +7665,20 @@ "type": "tidelift" } ], - "time": "2024-11-05T15:34:55+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "v3.5.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-12-11T03:49:26+00:00" }, { "name": "symfony/filesystem", - "version": "v7.1.6", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "c835867b3c62bb05c7fe3d637c871c7ae52024d4" + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/c835867b3c62bb05c7fe3d637c871c7ae52024d4", - "reference": "c835867b3c62bb05c7fe3d637c871c7ae52024d4", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", "shasum": "" }, "require": { @@ -6128,7 +7715,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.1.6" + "source": "https://github.com/symfony/filesystem/tree/v7.2.0" }, "funding": [ { @@ -6144,20 +7731,20 @@ "type": "tidelift" } ], - "time": "2024-10-25T15:11:02+00:00" + "time": "2024-10-25T15:15:23+00:00" }, { "name": "symfony/finder", - "version": "v7.1.6", + "version": "v7.2.2", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8" + "reference": "87a71856f2f56e4100373e92529eed3171695cfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2cb89664897be33f78c65d3d2845954c8d7a43b8", - "reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8", + "url": "https://api.github.com/repos/symfony/finder/zipball/87a71856f2f56e4100373e92529eed3171695cfb", + "reference": "87a71856f2f56e4100373e92529eed3171695cfb", "shasum": "" }, "require": { @@ -6192,7 +7779,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.1.6" + "source": "https://github.com/symfony/finder/tree/v7.2.2" }, "funding": [ { @@ -6208,20 +7795,20 @@ "type": "tidelift" } ], - "time": "2024-10-01T08:31:23+00:00" + "time": "2024-12-30T19:00:17+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.1.6", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "85e95eeede2d41cd146146e98c9c81d9214cae85" + "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/85e95eeede2d41cd146146e98c9c81d9214cae85", - "reference": "85e95eeede2d41cd146146e98c9c81d9214cae85", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/7da8fbac9dcfef75ffc212235d76b2754ce0cf50", + "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50", "shasum": "" }, "require": { @@ -6259,7 +7846,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.1.6" + "source": "https://github.com/symfony/options-resolver/tree/v7.2.0" }, "funding": [ { @@ -6275,7 +7862,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-11-20T11:17:29+00:00" }, { "name": "symfony/polyfill-ctype", @@ -6303,8 +7890,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -6379,8 +7966,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -6457,8 +8044,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -6535,8 +8122,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -6593,16 +8180,16 @@ }, { "name": "symfony/process", - "version": "v7.1.7", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "9b8a40b7289767aa7117e957573c2a535efe6585" + "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/9b8a40b7289767aa7117e957573c2a535efe6585", - "reference": "9b8a40b7289767aa7117e957573c2a535efe6585", + "url": "https://api.github.com/repos/symfony/process/zipball/d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", + "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", "shasum": "" }, "require": { @@ -6634,7 +8221,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.1.7" + "source": "https://github.com/symfony/process/tree/v7.2.0" }, "funding": [ { @@ -6650,103 +8237,20 @@ "type": "tidelift" } ], - "time": "2024-11-06T09:25:12+00:00" - }, - { - "name": "symfony/service-contracts", - "version": "v3.5.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "psr/container": "^1.1|^2.0", - "symfony/deprecation-contracts": "^2.5|^3" - }, - "conflict": { - "ext-psr": "<1.1|>=2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\Service\\": "" - }, - "exclude-from-classmap": [ - "/Test/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to writing services", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-11-06T14:24:19+00:00" }, { "name": "symfony/string", - "version": "v7.1.6", + "version": "v7.2.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "61b72d66bf96c360a727ae6232df5ac83c71f626" + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/61b72d66bf96c360a727ae6232df5ac83c71f626", - "reference": "61b72d66bf96c360a727ae6232df5ac83c71f626", + "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", "shasum": "" }, "require": { @@ -6804,7 +8308,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.1.6" + "source": "https://github.com/symfony/string/tree/v7.2.0" }, "funding": [ { @@ -6820,7 +8324,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-11-13T13:31:26+00:00" }, { "name": "textalk/websocket", @@ -6923,16 +8427,16 @@ }, { "name": "twig/twig", - "version": "v3.14.1", + "version": "v3.14.2", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "f405356d20fb43603bcadc8b09bfb676cb04a379" + "reference": "0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/f405356d20fb43603bcadc8b09bfb676cb04a379", - "reference": "f405356d20fb43603bcadc8b09bfb676cb04a379", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a", + "reference": "0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a", "shasum": "" }, "require": { @@ -6986,7 +8490,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.14.1" + "source": "https://github.com/twigphp/Twig/tree/v3.14.2" }, "funding": [ { @@ -6998,7 +8502,7 @@ "type": "tidelift" } ], - "time": "2024-11-06T18:17:38+00:00" + "time": "2024-11-07T12:36:22+00:00" }, { "name": "webmozart/glob", @@ -7052,7 +8556,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -7076,5 +8580,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.2.0" } diff --git a/docker-compose.yml b/docker-compose.yml index 5daf236ea4..9adbe55ae3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/docs/examples/1.6.x/client-graphql/examples/account/create-push-target.md b/docs/examples/1.6.x/client-graphql/examples/account/create-push-target.md index 8a0fad387c..63802a782e 100644 --- a/docs/examples/1.6.x/client-graphql/examples/account/create-push-target.md +++ b/docs/examples/1.6.x/client-graphql/examples/account/create-push-target.md @@ -12,5 +12,6 @@ mutation { providerId providerType identifier + expired } } diff --git a/docs/examples/1.6.x/client-graphql/examples/account/create.md b/docs/examples/1.6.x/client-graphql/examples/account/create.md index 3f8e3c3cf7..0d39394a3d 100644 --- a/docs/examples/1.6.x/client-graphql/examples/account/create.md +++ b/docs/examples/1.6.x/client-graphql/examples/account/create.md @@ -33,6 +33,7 @@ mutation { providerId providerType identifier + expired } accessedAt } diff --git a/docs/examples/1.6.x/client-graphql/examples/account/get.md b/docs/examples/1.6.x/client-graphql/examples/account/get.md index e4db8f0e41..f4f07c187f 100644 --- a/docs/examples/1.6.x/client-graphql/examples/account/get.md +++ b/docs/examples/1.6.x/client-graphql/examples/account/get.md @@ -28,6 +28,7 @@ query { providerId providerType identifier + expired } accessedAt } diff --git a/docs/examples/1.6.x/client-graphql/examples/account/update-email.md b/docs/examples/1.6.x/client-graphql/examples/account/update-email.md index b207bad4bb..c879e24a43 100644 --- a/docs/examples/1.6.x/client-graphql/examples/account/update-email.md +++ b/docs/examples/1.6.x/client-graphql/examples/account/update-email.md @@ -31,6 +31,7 @@ mutation { providerId providerType identifier + expired } accessedAt } diff --git a/docs/examples/1.6.x/client-graphql/examples/account/update-m-f-a.md b/docs/examples/1.6.x/client-graphql/examples/account/update-m-f-a.md index d2cd3d6ae5..787c2e0860 100644 --- a/docs/examples/1.6.x/client-graphql/examples/account/update-m-f-a.md +++ b/docs/examples/1.6.x/client-graphql/examples/account/update-m-f-a.md @@ -30,6 +30,7 @@ mutation { providerId providerType identifier + expired } accessedAt } diff --git a/docs/examples/1.6.x/client-graphql/examples/account/update-mfa-authenticator.md b/docs/examples/1.6.x/client-graphql/examples/account/update-mfa-authenticator.md index c74062c7d4..9cfe9150be 100644 --- a/docs/examples/1.6.x/client-graphql/examples/account/update-mfa-authenticator.md +++ b/docs/examples/1.6.x/client-graphql/examples/account/update-mfa-authenticator.md @@ -31,6 +31,7 @@ mutation { providerId providerType identifier + expired } accessedAt } diff --git a/docs/examples/1.6.x/client-graphql/examples/account/update-name.md b/docs/examples/1.6.x/client-graphql/examples/account/update-name.md index 850b5760a0..8ba2c99d9c 100644 --- a/docs/examples/1.6.x/client-graphql/examples/account/update-name.md +++ b/docs/examples/1.6.x/client-graphql/examples/account/update-name.md @@ -30,6 +30,7 @@ mutation { providerId providerType identifier + expired } accessedAt } diff --git a/docs/examples/1.6.x/client-graphql/examples/account/update-password.md b/docs/examples/1.6.x/client-graphql/examples/account/update-password.md index 5904da0842..f3619a10d2 100644 --- a/docs/examples/1.6.x/client-graphql/examples/account/update-password.md +++ b/docs/examples/1.6.x/client-graphql/examples/account/update-password.md @@ -31,6 +31,7 @@ mutation { providerId providerType identifier + expired } accessedAt } diff --git a/docs/examples/1.6.x/client-graphql/examples/account/update-phone.md b/docs/examples/1.6.x/client-graphql/examples/account/update-phone.md index 408a203300..adecb71168 100644 --- a/docs/examples/1.6.x/client-graphql/examples/account/update-phone.md +++ b/docs/examples/1.6.x/client-graphql/examples/account/update-phone.md @@ -31,6 +31,7 @@ mutation { providerId providerType identifier + expired } accessedAt } diff --git a/docs/examples/1.6.x/client-graphql/examples/account/update-prefs.md b/docs/examples/1.6.x/client-graphql/examples/account/update-prefs.md index 40db7b43db..57280247e4 100644 --- a/docs/examples/1.6.x/client-graphql/examples/account/update-prefs.md +++ b/docs/examples/1.6.x/client-graphql/examples/account/update-prefs.md @@ -30,6 +30,7 @@ mutation { providerId providerType identifier + expired } accessedAt } diff --git a/docs/examples/1.6.x/client-graphql/examples/account/update-push-target.md b/docs/examples/1.6.x/client-graphql/examples/account/update-push-target.md index 059702dc97..3c402cdf12 100644 --- a/docs/examples/1.6.x/client-graphql/examples/account/update-push-target.md +++ b/docs/examples/1.6.x/client-graphql/examples/account/update-push-target.md @@ -11,5 +11,6 @@ mutation { providerId providerType identifier + expired } } diff --git a/docs/examples/1.6.x/client-graphql/examples/account/update-status.md b/docs/examples/1.6.x/client-graphql/examples/account/update-status.md index aca8c098e7..c17f556842 100644 --- a/docs/examples/1.6.x/client-graphql/examples/account/update-status.md +++ b/docs/examples/1.6.x/client-graphql/examples/account/update-status.md @@ -28,6 +28,7 @@ mutation { providerId providerType identifier + expired } accessedAt } diff --git a/docs/examples/1.6.x/client-graphql/examples/messaging/create-subscriber.md b/docs/examples/1.6.x/client-graphql/examples/messaging/create-subscriber.md index b2712ebb48..bab53612b7 100644 --- a/docs/examples/1.6.x/client-graphql/examples/messaging/create-subscriber.md +++ b/docs/examples/1.6.x/client-graphql/examples/messaging/create-subscriber.md @@ -17,6 +17,7 @@ mutation { providerId providerType identifier + expired } userId userName diff --git a/docs/references/account/create-token-magic-url.md b/docs/references/account/create-token-magic-url.md index 6ebe4154b8..99ad6dba5e 100644 --- a/docs/references/account/create-token-magic-url.md +++ b/docs/references/account/create-token-magic-url.md @@ -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). diff --git a/src/Appwrite/Auth/OAuth2/Firebase.php b/src/Appwrite/Auth/OAuth2/Firebase.php deleted file mode 100644 index 0e2859e32c..0000000000 --- a/src/Appwrite/Auth/OAuth2/Firebase.php +++ /dev/null @@ -1,389 +0,0 @@ - '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; - } -} diff --git a/src/Appwrite/Certificates/Adapter.php b/src/Appwrite/Certificates/Adapter.php new file mode 100644 index 0000000000..711e4c09b9 --- /dev/null +++ b/src/Appwrite/Certificates/Adapter.php @@ -0,0 +1,14 @@ +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}"); + } + } +} diff --git a/src/Appwrite/Event/Usage.php b/src/Appwrite/Event/Usage.php index 161c251c8e..89e900d2ab 100644 --- a/src/Appwrite/Event/Usage.php +++ b/src/Appwrite/Event/Usage.php @@ -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; } } diff --git a/src/Appwrite/Event/Webhook.php b/src/Appwrite/Event/Webhook.php index 125c9a78d5..36c6923cae 100644 --- a/src/Appwrite/Event/Webhook.php +++ b/src/Appwrite/Event/Webhook.php @@ -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(); + } } diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php index cee1b2d263..19f69b1a4f 100644 --- a/src/Appwrite/Migration/Migration.php +++ b/src/Appwrite/Migration/Migration.php @@ -91,6 +91,7 @@ abstract class Migration '1.5.10' => 'V20', '1.5.11' => 'V20', '1.6.0' => 'V21', + '1.6.1' => 'V21', ]; /** diff --git a/src/Appwrite/Platform/Tasks/Maintenance.php b/src/Appwrite/Platform/Tasks/Maintenance.php index afb38f35fc..c789cbdaac 100644 --- a/src/Appwrite/Platform/Tasks/Maintenance.php +++ b/src/Appwrite/Platform/Tasks/Maintenance.php @@ -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) ]); diff --git a/src/Appwrite/Platform/Tasks/Migrate.php b/src/Appwrite/Platform/Tasks/Migrate.php index dcba59bb1d..4efa78ed4b 100644 --- a/src/Appwrite/Platform/Tasks/Migrate.php +++ b/src/Appwrite/Platform/Tasks/Migrate.php @@ -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; diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 65d2f7717c..6e37f270a4 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -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; diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index a1b85c341f..dad2db0d9a 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -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); }); } } diff --git a/src/Appwrite/Platform/Tasks/ScheduleExecutions.php b/src/Appwrite/Platform/Tasks/ScheduleExecutions.php index 73a2814397..086bad513e 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleExecutions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleExecutions.php @@ -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'], ); diff --git a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php index 4d57902330..c443bb6c2d 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php @@ -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 diff --git a/src/Appwrite/Platform/Tasks/ScheduleMessages.php b/src/Appwrite/Platform/Tasks/ScheduleMessages.php index b9d8e2a282..5d997fc5bb 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleMessages.php +++ b/src/Appwrite/Platform/Tasks/ScheduleMessages.php @@ -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'], ); diff --git a/src/Appwrite/Platform/Tasks/Specs.php b/src/Appwrite/Platform/Tasks/Specs.php index f71de98d95..0f1332d821 100644 --- a/src/Appwrite/Platform/Tasks/Specs.php +++ b/src/Appwrite/Platform/Tasks/Specs.php @@ -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 = [ diff --git a/src/Appwrite/Platform/Workers/Builds.php b/src/Appwrite/Platform/Workers/Builds.php index 5dd2f7f886..bef78a7514 100644 --- a/src/Appwrite/Platform/Workers/Builds.php +++ b/src/Appwrite/Platform/Workers/Builds.php @@ -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); } } } diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php index e55baa4e1e..0aff598781 100644 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -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(); diff --git a/src/Appwrite/Platform/Workers/Databases.php b/src/Appwrite/Platform/Workers/Databases.php index fe81114ea0..9c11dd4090 100644 --- a/src/Appwrite/Platform/Workers/Databases.php +++ b/src/Appwrite/Platform/Workers/Databases.php @@ -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)) { diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 48e4014f1e..9aaf19f412 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -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', '') diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 4b1926ed26..f6af0eb5f2 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -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(); } } } diff --git a/src/Appwrite/Platform/Workers/Usage.php b/src/Appwrite/Platform/Workers/Usage.php index 034e558d5d..8199fe73a7 100644 --- a/src/Appwrite/Platform/Workers/Usage.php +++ b/src/Appwrite/Platform/Workers/Usage.php @@ -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'); diff --git a/src/Appwrite/Platform/Workers/UsageDump.php b/src/Appwrite/Platform/Workers/UsageDump.php index b5480e1dec..3e50ba0363 100644 --- a/src/Appwrite/Platform/Workers/UsageDump.php +++ b/src/Appwrite/Platform/Workers/UsageDump.php @@ -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); diff --git a/src/Appwrite/Platform/Workers/Webhooks.php b/src/Appwrite/Platform/Workers/Webhooks.php index 88ca7871f2..50f64ea02d 100644 --- a/src/Appwrite/Platform/Workers/Webhooks.php +++ b/src/Appwrite/Platform/Workers/Webhooks.php @@ -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), ]); diff --git a/src/Appwrite/Specification/Format/OpenAPI3.php b/src/Appwrite/Specification/Format/OpenAPI3.php index f7430ec70e..65df0cb694 100644 --- a/src/Appwrite/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/Specification/Format/OpenAPI3.php @@ -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'] = '{}'; diff --git a/src/Appwrite/Specification/Format/Swagger2.php b/src/Appwrite/Specification/Format/Swagger2.php index f2e324c71f..66a540a4d4 100644 --- a/src/Appwrite/Specification/Format/Swagger2.php +++ b/src/Appwrite/Specification/Format/Swagger2.php @@ -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'] = '{}'; diff --git a/src/Appwrite/Utopia/Response/Model/Func.php b/src/Appwrite/Utopia/Response/Model/Func.php index f4ff214d0b..ab2d7ba051 100644 --- a/src/Appwrite/Utopia/Response/Model/Func.php +++ b/src/Appwrite/Utopia/Response/Model/Func.php @@ -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 * * *', ]) diff --git a/tests/e2e/Services/Databases/DatabasesCustomServerTest.php b/tests/e2e/Services/Databases/DatabasesCustomServerTest.php index 7cb8adb815..b501e2119e 100644 --- a/tests/e2e/Services/Databases/DatabasesCustomServerTest.php +++ b/tests/e2e/Services/Databases/DatabasesCustomServerTest.php @@ -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() diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 7b0847126c..326443afcf 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -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 */ diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index 616f309fd2..dda524fc7c 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -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(); diff --git a/tests/e2e/Services/Teams/TeamsBaseClient.php b/tests/e2e/Services/Teams/TeamsBaseClient.php index 9188575932..ca6082f6d0 100644 --- a/tests/e2e/Services/Teams/TeamsBaseClient.php +++ b/tests/e2e/Services/Teams/TeamsBaseClient.php @@ -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 diff --git a/tests/e2e/Services/Teams/TeamsCustomClientTest.php b/tests/e2e/Services/Teams/TeamsCustomClientTest.php index 03cb1983f1..7286bb0827 100644 --- a/tests/e2e/Services/Teams/TeamsCustomClientTest.php +++ b/tests/e2e/Services/Teams/TeamsCustomClientTest.php @@ -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]); } /** diff --git a/tests/e2e/Services/Users/UsersBase.php b/tests/e2e/Services/Users/UsersBase.php index bd0a8ef937..bbf9a5e2df 100644 --- a/tests/e2e/Services/Users/UsersBase.php +++ b/tests/e2e/Services/Users/UsersBase.php @@ -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']); }