mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a229cc1367 |
@@ -161,27 +161,6 @@ jobs:
|
||||
- name: Run PHPStan
|
||||
run: composer analyze -- --no-progress
|
||||
|
||||
specs:
|
||||
name: Checks / Specs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
extensions: swoole
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
|
||||
|
||||
- name: Generate specs
|
||||
run: _APP_STORAGE_LIMIT=5368709120 php app/cli.php specs --version=latest --git=no
|
||||
|
||||
locale:
|
||||
name: Checks / Locale
|
||||
runs-on: ubuntu-latest
|
||||
@@ -480,10 +459,6 @@ jobs:
|
||||
_APP_BROWSER_HOST: http://invalid-browser/v1
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
@@ -558,10 +533,6 @@ jobs:
|
||||
_APP_OPTIONS_ABUSE: enabled
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
@@ -619,10 +590,6 @@ jobs:
|
||||
env:
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
|
||||
@@ -44,7 +44,7 @@ Table of Contents:
|
||||
|
||||
## Products
|
||||
|
||||
- **[Appwrite Auth](https://appwrite.io/docs/products/auth)** - Secure user authentication with multiple login methods including email/password, SMS, OAuth, anonymous sessions, and magic links. Includes session management, multi-factor authentication, and user verification flows.
|
||||
- **[Appwrite Auth](https://appwrite.io/docs/products/authentication)** - Secure user authentication with multiple login methods including email/password, SMS, OAuth, anonymous sessions, and magic links. Includes session management, multi-factor authentication, and user verification flows.
|
||||
|
||||
- **[Appwrite Databases](https://appwrite.io/docs/products/databases)** - Scalable structured data storage with support for databases, tables, and rows. Includes querying, pagination, indexing, and relationships to model complex application data.
|
||||
|
||||
|
||||
+38
-34
@@ -18,15 +18,13 @@ use Swoole\Timer;
|
||||
use Utopia\Cache\Adapter\Pool as CachePool;
|
||||
use Utopia\Cache\Adapter\Sharding;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\CLI\Adapters\Generic;
|
||||
use Utopia\CLI\CLI;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Adapter\Pool as DatabasePool;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DI\Container;
|
||||
use Utopia\DI\Dependency;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Platform\Service;
|
||||
@@ -49,7 +47,7 @@ require_once __DIR__ . '/controllers/general.php';
|
||||
global $register;
|
||||
|
||||
$platform = new Appwrite();
|
||||
$args = $_SERVER['argv'] ?? [];
|
||||
$args = $platform->getEnv('argv');
|
||||
|
||||
\array_shift($args);
|
||||
if (! isset($args[0])) {
|
||||
@@ -58,15 +56,21 @@ if (! isset($args[0])) {
|
||||
}
|
||||
|
||||
$taskName = $args[0];
|
||||
$container = new Container();
|
||||
$cli = new CLI(new Generic(), $_SERVER['argv'] ?? [], $container);
|
||||
|
||||
$platform->setCli($cli);
|
||||
$platform->init(Service::TYPE_TASK);
|
||||
$cli = $platform->getCli();
|
||||
|
||||
$container->set('register', fn () => $register, []);
|
||||
$setResource = function (string $name, callable $callback, array $injections = []) use ($cli) {
|
||||
$dependency = new Dependency();
|
||||
$dependency->setName($name)->setCallback($callback);
|
||||
foreach ($injections as $injection) {
|
||||
$dependency->inject($injection);
|
||||
}
|
||||
$cli->setResource($dependency);
|
||||
};
|
||||
|
||||
$container->set('cache', function ($pools) {
|
||||
$setResource('register', fn () => $register, []);
|
||||
|
||||
$setResource('cache', function ($pools) {
|
||||
$list = Config::getParam('pools-cache', []);
|
||||
$adapters = [];
|
||||
|
||||
@@ -77,18 +81,18 @@ $container->set('cache', function ($pools) {
|
||||
return new Cache(new Sharding($adapters));
|
||||
}, ['pools']);
|
||||
|
||||
$container->set('pools', function (Registry $register) {
|
||||
$setResource('pools', function (Registry $register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
|
||||
$container->set('authorization', function () {
|
||||
$setResource('authorization', function () {
|
||||
$authorization = new Authorization();
|
||||
$authorization->disable();
|
||||
|
||||
return $authorization;
|
||||
}, []);
|
||||
|
||||
$container->set('dbForPlatform', function ($pools, $cache, $authorization) {
|
||||
$setResource('dbForPlatform', function ($pools, $cache, $authorization) {
|
||||
$sleep = 3;
|
||||
$maxAttempts = 5;
|
||||
$attempts = 0;
|
||||
@@ -131,17 +135,17 @@ $container->set('dbForPlatform', function ($pools, $cache, $authorization) {
|
||||
return $dbForPlatform;
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
$container->set('console', function () {
|
||||
$setResource('console', function () {
|
||||
return new Document(Config::getParam('console'));
|
||||
}, []);
|
||||
|
||||
$container->set(
|
||||
$setResource(
|
||||
'isResourceBlocked',
|
||||
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false,
|
||||
[]
|
||||
);
|
||||
|
||||
$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) {
|
||||
$setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) {
|
||||
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
|
||||
|
||||
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) {
|
||||
@@ -203,10 +207,10 @@ $container->set('getProjectDB', function (Group $pools, Database $dbForPlatform,
|
||||
};
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
|
||||
|
||||
$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
$setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
$database = null;
|
||||
|
||||
return function (?Document $project = null) use ($pools, $cache, &$database, $authorization) {
|
||||
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
|
||||
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant($project->getSequence());
|
||||
return $database;
|
||||
@@ -231,41 +235,41 @@ $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
$container->set('publisher', function (Group $pools) {
|
||||
$setResource('publisher', function (Group $pools) {
|
||||
return new BrokerPool(publisher: $pools->get('publisher'));
|
||||
}, ['pools']);
|
||||
$container->set('publisherDatabases', function (BrokerPool $publisher) {
|
||||
$setResource('publisherDatabases', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
$container->set('publisherFunctions', function (BrokerPool $publisher) {
|
||||
$setResource('publisherFunctions', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
$container->set('publisherMigrations', function (BrokerPool $publisher) {
|
||||
$setResource('publisherMigrations', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
$container->set('publisherMessaging', function (BrokerPool $publisher) {
|
||||
$setResource('publisherMessaging', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
$container->set('usage', function () {
|
||||
$setResource('usage', function () {
|
||||
return new UsageContext();
|
||||
}, []);
|
||||
$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
|
||||
$setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$container->set('queueForStatsResources', function (Publisher $publisher) {
|
||||
$setResource('queueForStatsResources', function (Publisher $publisher) {
|
||||
return new StatsResources($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('queueForFunctions', function (Publisher $publisher) {
|
||||
$setResource('queueForFunctions', function (Publisher $publisher) {
|
||||
return new Func($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('queueForDeletes', function (Publisher $publisher) {
|
||||
$setResource('queueForDeletes', function (Publisher $publisher) {
|
||||
return new Delete($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('queueForCertificates', function (Publisher $publisher) {
|
||||
$setResource('queueForCertificates', function (Publisher $publisher) {
|
||||
return new Certificate($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('logError', function (Registry $register) {
|
||||
$setResource('logError', function (Registry $register) {
|
||||
return function (Throwable $error, string $namespace, string $action) use ($register) {
|
||||
Console::error('[Error] Timestamp: ' . date('c', time()));
|
||||
Console::error('[Error] Type: ' . get_class($error));
|
||||
@@ -317,13 +321,13 @@ $container->set('logError', function (Registry $register) {
|
||||
};
|
||||
}, ['register']);
|
||||
|
||||
$container->set('executor', fn () => new Executor(), []);
|
||||
$setResource('executor', fn () => new Executor(), []);
|
||||
|
||||
$container->set('bus', function (Registry $register) use ($container) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $container->get($name));
|
||||
$setResource('bus', function (Registry $register) use ($cli) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $cli->getResource($name));
|
||||
}, ['register']);
|
||||
|
||||
$container->set('telemetry', fn () => new NoTelemetry(), []);
|
||||
$setResource('telemetry', fn () => new NoTelemetry(), []);
|
||||
|
||||
$exitCode = 0;
|
||||
|
||||
|
||||
@@ -594,7 +594,7 @@ $platformCollections = [
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('key'), // For app platforms
|
||||
'$id' => ID::custom('key'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
@@ -605,7 +605,7 @@ $platformCollections = [
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('store'), // Unused at the moment
|
||||
'$id' => ID::custom('store'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 256,
|
||||
@@ -616,7 +616,7 @@ $platformCollections = [
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('hostname'), // For web platforms
|
||||
'$id' => ID::custom('hostname'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 256,
|
||||
|
||||
@@ -1179,16 +1179,6 @@ return [
|
||||
'description' => 'Platform with the requested ID could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::PLATFORM_METHOD_UNSUPPORTED => [
|
||||
'name' => Exception::PLATFORM_METHOD_UNSUPPORTED,
|
||||
'description' => 'The requested platform has invalid type. Please use corresponding update method for the platform type.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::PLATFORM_ALREADY_EXISTS => [
|
||||
'name' => Exception::PLATFORM_ALREADY_EXISTS,
|
||||
'description' => 'Platform with the same ID already exists in this project. Try again with a different ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::VARIABLE_NOT_FOUND => [
|
||||
'name' => Exception::VARIABLE_NOT_FOUND,
|
||||
'description' => 'Variable with the requested ID could not be found.',
|
||||
|
||||
@@ -376,17 +376,6 @@ return [
|
||||
'mock' => false,
|
||||
'class' => 'Appwrite\\Auth\\OAuth2\\Wordpress',
|
||||
],
|
||||
'x' => [
|
||||
'name' => 'X',
|
||||
'developers' => 'https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code',
|
||||
'icon' => 'icon-twitter',
|
||||
'enabled' => true,
|
||||
'sandbox' => false,
|
||||
'form' => false,
|
||||
'beta' => false,
|
||||
'mock' => false,
|
||||
'class' => 'Appwrite\\Auth\\OAuth2\\X',
|
||||
],
|
||||
'yahoo' => [
|
||||
'name' => 'Yahoo',
|
||||
'developers' => 'https://developer.yahoo.com/oauth2/guide/flows_authcode/',
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
// List of scopes for organization (teams) API keys
|
||||
|
||||
return [
|
||||
"platforms.read" => [
|
||||
"description" => 'Access to read project\'s platforms',
|
||||
],
|
||||
"platforms.write" => [
|
||||
"description" =>
|
||||
'Access to create, update, and delete project\'s platforms',
|
||||
],
|
||||
"projects.read" => [
|
||||
"description" => 'Access to read organization\'s projects',
|
||||
],
|
||||
@@ -10,6 +17,13 @@ return [
|
||||
"description" =>
|
||||
"Access to create, update, and delete projects in organization",
|
||||
],
|
||||
"keys.read" => [
|
||||
"description" => 'Access to read project\'s API keys',
|
||||
],
|
||||
"keys.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s API keys",
|
||||
],
|
||||
"devKeys.read" => [
|
||||
"description" => 'Access to read project\'s development keys',
|
||||
],
|
||||
|
||||
@@ -188,20 +188,4 @@ return [ // List of publicly visible scopes
|
||||
"description" =>
|
||||
"Access to update project\'s information",
|
||||
],
|
||||
"keys.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s keys",
|
||||
],
|
||||
"keys.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s keys",
|
||||
],
|
||||
"platforms.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s platforms",
|
||||
],
|
||||
"platforms.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s platforms",
|
||||
],
|
||||
];
|
||||
|
||||
@@ -207,7 +207,7 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr
|
||||
}
|
||||
|
||||
|
||||
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) {
|
||||
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, Authorization $authorization) {
|
||||
|
||||
// Attempt to decode secret as a JWT (used by OAuth2 token flow to carry provider info)
|
||||
$oauthProvider = null;
|
||||
@@ -345,7 +345,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||
->setProperty('secret', $sessionSecret)
|
||||
->encode();
|
||||
|
||||
if (!$domainVerification) {
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
|
||||
}
|
||||
|
||||
@@ -353,8 +353,8 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||
$protocol = $request->getProtocol();
|
||||
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED);
|
||||
|
||||
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
|
||||
@@ -719,9 +719,7 @@ Http::delete('/v1/account/sessions')
|
||||
->inject('queueForDeletes')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
|
||||
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken) {
|
||||
|
||||
$protocol = $request->getProtocol();
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
@@ -730,7 +728,7 @@ Http::delete('/v1/account/sessions')
|
||||
foreach ($sessions as $session) {/** @var Document $session */
|
||||
$dbForProject->deleteDocument('sessions', $session->getId());
|
||||
|
||||
if (!$domainVerification) {
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
|
||||
}
|
||||
|
||||
@@ -743,8 +741,8 @@ Http::delete('/v1/account/sessions')
|
||||
|
||||
// If current session delete the cookies too
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
|
||||
// Use current session for events.
|
||||
$currentSession = $session;
|
||||
@@ -851,9 +849,7 @@ Http::delete('/v1/account/sessions/:sessionId')
|
||||
->inject('queueForDeletes')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
|
||||
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken) {
|
||||
|
||||
$protocol = $request->getProtocol();
|
||||
$sessionId = ($sessionId === 'current')
|
||||
@@ -879,13 +875,13 @@ Http::delete('/v1/account/sessions/:sessionId')
|
||||
->setAttribute('current', true)
|
||||
->setAttribute('countryName', $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')));
|
||||
|
||||
if (!$domainVerification) {
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
|
||||
}
|
||||
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
@@ -1039,10 +1035,8 @@ Http::post('/v1/account/sessions/email')
|
||||
->inject('store')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->inject('authorization')
|
||||
->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) {
|
||||
->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) {
|
||||
$email = \strtolower($email);
|
||||
$protocol = $request->getProtocol();
|
||||
|
||||
@@ -1103,28 +1097,28 @@ Http::post('/v1/account/sessions/email')
|
||||
]));
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$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())),
|
||||
]));
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$encoded = $store
|
||||
->setProperty('id', $user->getId())
|
||||
->setProperty('secret', $secret)
|
||||
->encode();
|
||||
|
||||
if (!$domainVerification) {
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
|
||||
}
|
||||
|
||||
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
|
||||
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
;
|
||||
|
||||
@@ -1190,10 +1184,8 @@ Http::post('/v1/account/sessions/anonymous')
|
||||
->inject('store')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->inject('authorization')
|
||||
->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) {
|
||||
->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) {
|
||||
$protocol = $request->getProtocol();
|
||||
|
||||
if ('console' === $project->getId()) {
|
||||
@@ -1284,15 +1276,15 @@ Http::post('/v1/account/sessions/anonymous')
|
||||
->setProperty('secret', $secret)
|
||||
->encode();
|
||||
|
||||
if (!$domainVerification) {
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
|
||||
}
|
||||
|
||||
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
|
||||
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
;
|
||||
|
||||
@@ -1347,9 +1339,7 @@ Http::post('/v1/account/sessions/token')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->inject('proofForCode')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->inject('authorization')
|
||||
->inject('authorization')
|
||||
->action($createSession);
|
||||
|
||||
Http::get('/v1/account/sessions/oauth2/:provider')
|
||||
@@ -1548,10 +1538,8 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->inject('plan')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->inject('authorization')
|
||||
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) use ($oauthDefaultSuccess) {
|
||||
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, Authorization $authorization) use ($oauthDefaultSuccess) {
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$port = $request->getPort();
|
||||
$callbackBase = $protocol . '://' . $request->getHostname();
|
||||
@@ -2067,7 +2055,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->setProperty('secret', $secret)
|
||||
->encode();
|
||||
|
||||
if (!$domainVerification) {
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
|
||||
}
|
||||
|
||||
@@ -2080,14 +2068,14 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
// TODO: Remove this deprecated workaround - support only token
|
||||
if ($state['success']['path'] == $oauthDefaultSuccess) {
|
||||
$query['project'] = $project->getId();
|
||||
$query['domain'] = $cookieDomain;
|
||||
$query['domain'] = Config::getParam('cookieDomain');
|
||||
$query['key'] = $store->getKey();
|
||||
$query['secret'] = $encoded;
|
||||
}
|
||||
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
}
|
||||
|
||||
if (isset($sessionUpgrade) && $sessionUpgrade && isset($session)) {
|
||||
@@ -2185,7 +2173,7 @@ Http::get('/v1/account/tokens/oauth2/:provider')
|
||||
}
|
||||
|
||||
$host = $platform['consoleHostname'] ?? '';
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$port = $request->getPort();
|
||||
$redirectBase = $protocol . '://' . $host;
|
||||
if ($protocol === 'https' && $port !== '443') {
|
||||
@@ -2208,12 +2196,10 @@ Http::get('/v1/account/tokens/oauth2/:provider')
|
||||
'token' => true,
|
||||
], $scopes);
|
||||
|
||||
$loginURL = $oauth2->getLoginURL();
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->redirect($loginURL);
|
||||
->redirect($oauth2->getLoginURL());
|
||||
});
|
||||
|
||||
Http::post('/v1/account/tokens/magic-url')
|
||||
@@ -2900,13 +2886,11 @@ Http::put('/v1/account/sessions/magic-url')
|
||||
->inject('queueForMails')
|
||||
->inject('store')
|
||||
->inject('proofForCode')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->inject('authorization')
|
||||
->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $domainVerification, $cookieDomain, $authorization) use ($createSession) {
|
||||
->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $authorization) use ($createSession) {
|
||||
$proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL);
|
||||
$proofForToken->setHash(new Sha());
|
||||
$createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $domainVerification, $cookieDomain, $authorization);
|
||||
$createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $authorization);
|
||||
});
|
||||
|
||||
Http::put('/v1/account/sessions/phone')
|
||||
@@ -2952,8 +2936,6 @@ Http::put('/v1/account/sessions/phone')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->inject('proofForCode')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->inject('authorization')
|
||||
->action($createSession);
|
||||
|
||||
@@ -3745,9 +3727,7 @@ Http::patch('/v1/account/status')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('store')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store, bool $domainVerification, ?string $cookieDomain) {
|
||||
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store) {
|
||||
|
||||
$user->setAttribute('status', false);
|
||||
|
||||
@@ -3757,14 +3737,14 @@ Http::patch('/v1/account/status')
|
||||
->setParam('userId', $user->getId())
|
||||
->setPayload($response->output($user, Response::MODEL_ACCOUNT));
|
||||
|
||||
if (!$domainVerification) {
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
|
||||
}
|
||||
|
||||
$protocol = $request->getProtocol();
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
;
|
||||
|
||||
$response->dynamic($user, Response::MODEL_ACCOUNT);
|
||||
|
||||
@@ -5,18 +5,28 @@ use Appwrite\Auth\Validator\MockNumber;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Keys;
|
||||
use Appwrite\Utopia\Response;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Http\Http;
|
||||
@@ -759,6 +769,288 @@ Http::delete('/v1/projects/:projectId')
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
// Keys
|
||||
|
||||
Http::post('/v1/projects/:projectId/keys')
|
||||
->desc('Create key')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'keys.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'keys',
|
||||
name: 'createKey',
|
||||
description: '/docs/references/projects/create-key.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
model: Response::MODEL_KEY,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
// TODO: When migrating to Platform API, mark keyId required for consistency
|
||||
->param('keyId', 'unique()', fn (Database $dbForPlatform) => new CustomId($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', true, ['dbForPlatform'])->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
|
||||
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
|
||||
->param('expire', null, new Nullable(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('dbForPlatform')
|
||||
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
|
||||
$keyId = $keyId == 'unique()' ? ID::unique() : $keyId;
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$key = new Document([
|
||||
'$id' => $keyId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'resourceInternalId' => $project->getSequence(),
|
||||
'resourceId' => $project->getId(),
|
||||
'resourceType' => 'projects',
|
||||
'name' => $name,
|
||||
'scopes' => $scopes,
|
||||
'expire' => $expire,
|
||||
'sdks' => [],
|
||||
'accessedAt' => null,
|
||||
'secret' => API_KEY_STANDARD . '_' . \bin2hex(\random_bytes(128)),
|
||||
]);
|
||||
|
||||
try {
|
||||
$key = $dbForPlatform->createDocument('keys', $key);
|
||||
} catch (Duplicate) {
|
||||
throw new Exception(Exception::KEY_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($key, Response::MODEL_KEY);
|
||||
});
|
||||
|
||||
Http::get('/v1/projects/:projectId/keys')
|
||||
->desc('List keys')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'keys.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'keys',
|
||||
name: 'listKeys',
|
||||
description: '/docs/references/projects/list-keys.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_KEY_LIST,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('queries', [], new Keys(), '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(', ', Keys::ALLOWED_ATTRIBUTES), true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, array $queries, bool $includeTotal, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
if (\count(Query::getByType($queries, [Query::TYPE_LIMIT])) === 0) {
|
||||
$queries[] = Query::limit(5000);
|
||||
}
|
||||
|
||||
$queries[] = Query::equal('resourceType', ['projects']);
|
||||
$queries[] = Query::equal('resourceInternalId', [$project->getSequence()]);
|
||||
|
||||
$cursor = Query::getCursorQueries($queries, false);
|
||||
$cursor = \reset($cursor);
|
||||
|
||||
if ($cursor !== false) {
|
||||
$validator = new Cursor();
|
||||
if (!$validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$keyId = $cursor->getValue();
|
||||
$cursorDocument = $dbForPlatform->getDocument('keys', $keyId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Key '{$keyId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
$keys = $dbForPlatform->find('keys', $queries);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'keys' => $keys,
|
||||
'total' => $includeTotal ? $dbForPlatform->count('keys', $filterQueries, APP_LIMIT_COUNT) : 0,
|
||||
]), Response::MODEL_KEY_LIST);
|
||||
});
|
||||
|
||||
Http::get('/v1/projects/:projectId/keys/:keyId')
|
||||
->desc('Get key')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'keys.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'keys',
|
||||
name: 'getKey',
|
||||
description: '/docs/references/projects/get-key.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_KEY,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key unique ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $keyId, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$key = $dbForPlatform->findOne('keys', [
|
||||
Query::equal('$id', [$keyId]),
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($key->isEmpty()) {
|
||||
throw new Exception(Exception::KEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($key, Response::MODEL_KEY);
|
||||
});
|
||||
|
||||
Http::put('/v1/projects/:projectId/keys/:keyId')
|
||||
->desc('Update key')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'keys.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'keys',
|
||||
name: 'updateKey',
|
||||
description: '/docs/references/projects/update-key.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_KEY,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key unique ID.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
|
||||
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
|
||||
->param('expire', null, new Nullable(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('dbForPlatform')
|
||||
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$key = $dbForPlatform->findOne('keys', [
|
||||
Query::equal('$id', [$keyId]),
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($key->isEmpty()) {
|
||||
throw new Exception(Exception::KEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$key
|
||||
->setAttribute('name', $name)
|
||||
->setAttribute('scopes', $scopes)
|
||||
->setAttribute('expire', $expire);
|
||||
|
||||
$dbForPlatform->updateDocument('keys', $key->getId(), $key);
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->dynamic($key, Response::MODEL_KEY);
|
||||
});
|
||||
|
||||
Http::delete('/v1/projects/:projectId/keys/:keyId')
|
||||
->desc('Delete key')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'keys.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'keys',
|
||||
name: 'deleteKey',
|
||||
description: '/docs/references/projects/delete-key.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_NOCONTENT,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::NONE
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key unique ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $keyId, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$key = $dbForPlatform->findOne('keys', [
|
||||
Query::equal('$id', [$keyId]),
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($key->isEmpty()) {
|
||||
throw new Exception(Exception::KEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$dbForPlatform->deleteDocument('keys', $key->getId());
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
// JWT Keys
|
||||
|
||||
Http::post('/v1/projects/:projectId/jwts')
|
||||
@@ -801,6 +1093,272 @@ Http::post('/v1/projects/:projectId/jwts')
|
||||
])]), Response::MODEL_JWT);
|
||||
});
|
||||
|
||||
// Platforms
|
||||
|
||||
Http::post('/v1/projects/:projectId/platforms')
|
||||
->desc('Create platform')
|
||||
->groups(['api', 'projects'])
|
||||
->label('audits.event', 'platforms.create')
|
||||
->label('audits.resource', 'project/{request.projectId}')
|
||||
->label('scope', 'platforms.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'platforms',
|
||||
name: 'createPlatform',
|
||||
description: '/docs/references/projects/create-platform.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
model: Response::MODEL_PLATFORM,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param(
|
||||
'type',
|
||||
null,
|
||||
new WhiteList([
|
||||
Platform::TYPE_WEB,
|
||||
Platform::TYPE_FLUTTER_WEB,
|
||||
Platform::TYPE_FLUTTER_IOS,
|
||||
Platform::TYPE_FLUTTER_ANDROID,
|
||||
Platform::TYPE_FLUTTER_LINUX,
|
||||
Platform::TYPE_FLUTTER_MACOS,
|
||||
Platform::TYPE_FLUTTER_WINDOWS,
|
||||
Platform::TYPE_APPLE_IOS,
|
||||
Platform::TYPE_APPLE_MACOS,
|
||||
Platform::TYPE_APPLE_WATCHOS,
|
||||
Platform::TYPE_APPLE_TVOS,
|
||||
Platform::TYPE_ANDROID,
|
||||
Platform::TYPE_UNITY,
|
||||
Platform::TYPE_REACT_NATIVE_IOS,
|
||||
Platform::TYPE_REACT_NATIVE_ANDROID,
|
||||
], true),
|
||||
'Platform type. Possible values are: web, flutter-web, flutter-ios, flutter-android, flutter-linux, flutter-macos, flutter-windows, apple-ios, apple-macos, apple-watchos, apple-tvos, android, unity, react-native-ios, react-native-android.'
|
||||
)
|
||||
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
|
||||
->param('key', '', new Text(256), 'Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', true)
|
||||
->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('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);
|
||||
}
|
||||
|
||||
$platform = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'projectInternalId' => $project->getSequence(),
|
||||
'projectId' => $project->getId(),
|
||||
'type' => $type,
|
||||
'name' => $name,
|
||||
'key' => $key,
|
||||
'store' => $store,
|
||||
'hostname' => $hostname
|
||||
]);
|
||||
|
||||
$platform = $dbForPlatform->createDocument('platforms', $platform);
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($platform, Response::MODEL_PLATFORM);
|
||||
});
|
||||
|
||||
Http::get('/v1/projects/:projectId/platforms')
|
||||
->desc('List platforms')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'platforms.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'platforms',
|
||||
name: 'listPlatforms',
|
||||
description: '/docs/references/projects/list-platforms.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PLATFORM_LIST,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$platforms = $dbForPlatform->find('platforms', [
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
Query::limit(5000),
|
||||
]);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'platforms' => $platforms,
|
||||
'total' => $includeTotal ? count($platforms) : 0,
|
||||
]), Response::MODEL_PLATFORM_LIST);
|
||||
});
|
||||
|
||||
Http::get('/v1/projects/:projectId/platforms/:platformId')
|
||||
->desc('Get platform')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'platforms.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'platforms',
|
||||
name: 'getPlatform',
|
||||
description: '/docs/references/projects/get-platform.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PLATFORM,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform unique ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $platformId, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$platform = $dbForPlatform->findOne('platforms', [
|
||||
Query::equal('$id', [$platformId]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($platform->isEmpty()) {
|
||||
throw new Exception(Exception::PLATFORM_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($platform, Response::MODEL_PLATFORM);
|
||||
});
|
||||
|
||||
Http::put('/v1/projects/:projectId/platforms/:platformId')
|
||||
->desc('Update platform')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'platforms.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'platforms',
|
||||
name: 'updatePlatform',
|
||||
description: '/docs/references/projects/update-platform.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PLATFORM,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform unique ID.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
|
||||
->param('key', '', new Text(256), 'Package name for android or bundle ID for iOS. Max length: 256 chars.', true)
|
||||
->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('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 = $dbForPlatform->findOne('platforms', [
|
||||
Query::equal('$id', [$platformId]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($platform->isEmpty()) {
|
||||
throw new Exception(Exception::PLATFORM_NOT_FOUND);
|
||||
}
|
||||
|
||||
$platform
|
||||
->setAttribute('name', $name)
|
||||
->setAttribute('key', $key)
|
||||
->setAttribute('store', $store)
|
||||
->setAttribute('hostname', $hostname);
|
||||
|
||||
$dbForPlatform->updateDocument('platforms', $platform->getId(), $platform);
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->dynamic($platform, Response::MODEL_PLATFORM);
|
||||
});
|
||||
|
||||
Http::delete('/v1/projects/:projectId/platforms/:platformId')
|
||||
->desc('Delete platform')
|
||||
->groups(['api', 'projects'])
|
||||
->label('audits.event', 'platforms.delete')
|
||||
->label('audits.resource', 'project/{request.projectId}/platform/${request.platformId}')
|
||||
->label('scope', 'platforms.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'platforms',
|
||||
name: 'deletePlatform',
|
||||
description: '/docs/references/projects/delete-platform.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_NOCONTENT,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::NONE
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform unique ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $platformId, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$platform = $dbForPlatform->findOne('platforms', [
|
||||
Query::equal('$id', [$platformId]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($platform->isEmpty()) {
|
||||
throw new Exception(Exception::PLATFORM_NOT_FOUND);
|
||||
}
|
||||
|
||||
$dbForPlatform->deleteDocument('platforms', $platformId);
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
|
||||
// CUSTOM SMTP and Templates
|
||||
Http::patch('/v1/projects/:projectId/smtp')
|
||||
->desc('Update SMTP')
|
||||
|
||||
@@ -61,6 +61,8 @@ use Utopia\System\System;
|
||||
use Utopia\Validator;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
Config::setParam('domainVerification', false);
|
||||
Config::setParam('cookieDomain', 'localhost');
|
||||
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
|
||||
|
||||
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
|
||||
@@ -752,8 +754,11 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
}
|
||||
|
||||
if (\is_array($values)) {
|
||||
$count = 0;
|
||||
foreach ($values as $value) {
|
||||
$response->addHeader($name, $value);
|
||||
$override = $count === 0;
|
||||
$response->addHeader($name, $value, override: $override);
|
||||
$count++;
|
||||
}
|
||||
} else {
|
||||
$response->addHeader($name, $values);
|
||||
@@ -899,16 +904,40 @@ Http::init()
|
||||
$locale->setDefault($localeParam);
|
||||
}
|
||||
|
||||
$origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST);
|
||||
$selfDomain = new Domain($request->getHostname());
|
||||
$endDomain = new Domain((string)$origin);
|
||||
Config::setParam(
|
||||
'domainVerification',
|
||||
($selfDomain->getRegisterable() === $endDomain->getRegisterable()) &&
|
||||
$endDomain->getRegisterable() !== ''
|
||||
);
|
||||
|
||||
$localHosts = ['localhost','localhost:'.$request->getPort()];
|
||||
|
||||
$migrationHost = System::getEnv('_APP_MIGRATION_HOST');
|
||||
if (!empty($migrationHost)) {
|
||||
// Treat the migration host like localhost because internal migration and
|
||||
// CI traffic may use it before a public domain is configured.
|
||||
$localHosts[] = $migrationHost;
|
||||
$localHosts[] = $migrationHost.':'.$request->getPort();
|
||||
}
|
||||
|
||||
$isLocalHost = in_array($request->getHostname(), $localHosts);
|
||||
$isIpAddress = filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false;
|
||||
|
||||
$isConsoleProject = $project->getAttribute('$id', '') === 'console';
|
||||
$isConsoleRootSession = System::getEnv('_APP_CONSOLE_ROOT_SESSION', 'disabled') === 'enabled';
|
||||
|
||||
Config::setParam(
|
||||
'cookieDomain',
|
||||
$isLocalHost || $isIpAddress
|
||||
? null
|
||||
: (
|
||||
$isConsoleProject && $isConsoleRootSession
|
||||
? '.' . $selfDomain->getRegisterable()
|
||||
: '.' . $request->getHostname()
|
||||
)
|
||||
);
|
||||
|
||||
$warnings = [];
|
||||
|
||||
/*
|
||||
@@ -1471,9 +1500,7 @@ Http::error()
|
||||
try {
|
||||
$cors = $utopia->getResource('cors');
|
||||
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
|
||||
$response
|
||||
->removeHeader($name)
|
||||
->addHeader($name, $value);
|
||||
$response->addHeader($name, $value, override: true);
|
||||
}
|
||||
} catch (Throwable) {
|
||||
// Degrade gracefully - error response without CORS is no worse than before.
|
||||
|
||||
@@ -98,9 +98,6 @@ Http::init()
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
|
||||
$route = $utopia->getRoute();
|
||||
if ($route === null) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user authentication and session validation.
|
||||
@@ -492,10 +489,6 @@ Http::init()
|
||||
$request->setUser($user);
|
||||
|
||||
$route = $utopia->getRoute();
|
||||
if ($route === null) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$path = $route->getMatchedPath();
|
||||
$databaseType = match (true) {
|
||||
str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
|
||||
|
||||
+54
-57
@@ -1,13 +1,16 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/init.php';
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
require_once __DIR__ . '/init/span.php';
|
||||
|
||||
$registerRequestResources = require __DIR__ . '/init/resources/request.php';
|
||||
global $register;
|
||||
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Swoole\Constant;
|
||||
use Swoole\Http\Request as SwooleRequest;
|
||||
use Swoole\Http\Response as SwooleResponse;
|
||||
use Swoole\Http\Server;
|
||||
use Swoole\Process;
|
||||
use Swoole\Table;
|
||||
use Swoole\Timer;
|
||||
@@ -26,7 +29,6 @@ use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Http\Adapter\Swoole\Server;
|
||||
use Utopia\Http\Files;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Logger\Log;
|
||||
@@ -47,35 +49,18 @@ $certifiedDomains = new Table(100_000);
|
||||
$certifiedDomains->column('value', Table::TYPE_INT, 1);
|
||||
$certifiedDomains->create();
|
||||
|
||||
global $container;
|
||||
$container->set('riskyDomains', fn () => $riskyDomains);
|
||||
$container->set('certifiedDomains', fn () => $certifiedDomains);
|
||||
$container->set('pools', function ($register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
Http::setResource('riskyDomains', fn () => $riskyDomains);
|
||||
Http::setResource('certifiedDomains', fn () => $certifiedDomains);
|
||||
|
||||
$http = new Server(
|
||||
host: "0.0.0.0",
|
||||
port: System::getEnv('PORT', 80),
|
||||
mode: SWOOLE_PROCESS,
|
||||
);
|
||||
|
||||
$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
|
||||
$totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
|
||||
|
||||
$swooleAdapter = new Server(
|
||||
host: "0.0.0.0",
|
||||
port: System::getEnv('PORT', 80),
|
||||
settings: [
|
||||
Constant::OPTION_WORKER_NUM => $totalWorkers,
|
||||
Constant::OPTION_DISPATCH_FUNC => dispatch(...),
|
||||
Constant::OPTION_DISPATCH_MODE => SWOOLE_DISPATCH_UIDMOD,
|
||||
Constant::OPTION_HTTP_COMPRESSION => false,
|
||||
Constant::OPTION_PACKAGE_MAX_LENGTH => $payloadSize,
|
||||
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
|
||||
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
|
||||
],
|
||||
container: $container,
|
||||
);
|
||||
|
||||
$container->set('container', fn () => fn () => $swooleAdapter->getContainer());
|
||||
|
||||
$http = $swooleAdapter->getServer();
|
||||
|
||||
/**
|
||||
* Assigns HTTP requests to worker threads by analyzing its payload/content.
|
||||
*
|
||||
@@ -84,16 +69,16 @@ $http = $swooleAdapter->getServer();
|
||||
* 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 \Swoole\Http\Server $server Swoole server instance.
|
||||
* @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(\Swoole\Http\Server $server, int $fd, int $type, $data = null): int
|
||||
function dispatch(Server $server, int $fd, int $type, $data = null): int
|
||||
{
|
||||
$resolveWorkerId = function (\Swoole\Http\Server $server, $data = null) {
|
||||
$resolveWorkerId = function (Server $server, $data = null) {
|
||||
global $totalWorkers, $riskyDomains;
|
||||
|
||||
// If data is not set we can send request to any worker
|
||||
@@ -176,6 +161,18 @@ function dispatch(\Swoole\Http\Server $server, int $fd, int $type, $data = null)
|
||||
return $workerId;
|
||||
}
|
||||
|
||||
|
||||
$http
|
||||
->set([
|
||||
Constant::OPTION_WORKER_NUM => $totalWorkers,
|
||||
Constant::OPTION_DISPATCH_FUNC => dispatch(...),
|
||||
Constant::OPTION_DISPATCH_MODE => SWOOLE_DISPATCH_UIDMOD,
|
||||
Constant::OPTION_HTTP_COMPRESSION => false,
|
||||
Constant::OPTION_PACKAGE_MAX_LENGTH => $payloadSize,
|
||||
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
|
||||
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
|
||||
]);
|
||||
|
||||
$http->on(Constant::EVENT_WORKER_START, function ($server, $workerId) {
|
||||
});
|
||||
|
||||
@@ -192,14 +189,16 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
|
||||
Console::success('Reload completed...');
|
||||
});
|
||||
|
||||
$container->set('bus', function ($register) use ($swooleAdapter) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $swooleAdapter->getContainer()->get($name));
|
||||
}, ['register']);
|
||||
Http::setResource('bus', function ($register, $utopia) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $utopia->getResource($name));
|
||||
}, ['register', 'utopia']);
|
||||
|
||||
include __DIR__ . '/controllers/general.php';
|
||||
|
||||
function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
|
||||
{
|
||||
$max = 15;
|
||||
$sleep = 2;
|
||||
$max = 15;
|
||||
$sleep = 2;
|
||||
$attempts = 0;
|
||||
@@ -290,13 +289,13 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c
|
||||
Span::current()?->finish();
|
||||
}
|
||||
|
||||
$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $swooleAdapter) {
|
||||
$app = new Http($swooleAdapter, 'UTC');
|
||||
$http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $totalWorkers, $register) {
|
||||
$app = new Http('UTC');
|
||||
|
||||
/** @var \Utopia\Pools\Group $pools */
|
||||
$pools = $app->getResource('pools');
|
||||
|
||||
go(function () use ($app, $pools) {
|
||||
go(function () use ($register, $app) {
|
||||
$pools = $register->get('pools');
|
||||
/** @var \Utopia\Pools\Group $pools */
|
||||
Http::setResource('pools', fn () => $pools);
|
||||
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
@@ -512,11 +511,14 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke
|
||||
});
|
||||
});
|
||||
|
||||
$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swooleAdapter, $registerRequestResources) {
|
||||
$http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register, $files) {
|
||||
Span::init('http.request');
|
||||
|
||||
$request = new Request($utopiaRequest->getSwooleRequest());
|
||||
$response = new Response($utopiaResponse->getSwooleResponse());
|
||||
Http::setResource('swooleRequest', fn () => $swooleRequest);
|
||||
Http::setResource('swooleResponse', fn () => $swooleResponse);
|
||||
|
||||
$request = new Request($swooleRequest);
|
||||
$response = new Response($swooleResponse);
|
||||
|
||||
Span::add('http.method', $request->getMethod());
|
||||
|
||||
@@ -532,18 +534,13 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
|
||||
return;
|
||||
}
|
||||
|
||||
$requestContainer = $swooleAdapter->getContainer();
|
||||
$requestContainer->set('request', fn () => $request);
|
||||
$requestContainer->set('response', fn () => $response);
|
||||
|
||||
$app = new Http($swooleAdapter, 'UTC');
|
||||
$requestContainer->set('utopia', fn () => $app);
|
||||
|
||||
$registerRequestResources($requestContainer);
|
||||
|
||||
$app = new Http('UTC');
|
||||
$app->setCompression(System::getEnv('_APP_COMPRESSION_ENABLED', 'enabled') === 'enabled');
|
||||
$app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB
|
||||
|
||||
$pools = $register->get('pools');
|
||||
Http::setResource('pools', fn () => $pools);
|
||||
|
||||
try {
|
||||
$authorization = $app->getResource('authorization');
|
||||
|
||||
@@ -627,7 +624,6 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
|
||||
}
|
||||
}
|
||||
|
||||
$swooleResponse = $utopiaResponse->getSwooleResponse();
|
||||
$swooleResponse->setStatusCode(500);
|
||||
|
||||
$output = ((Http::isDevelopment())) ? [
|
||||
@@ -651,10 +647,11 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
|
||||
});
|
||||
|
||||
// Fetch domains every `DOMAIN_SYNC_TIMER` seconds and update in the memory
|
||||
$http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
|
||||
$http->on(Constant::EVENT_TASK, function () use ($register) {
|
||||
$lastSyncUpdate = null;
|
||||
|
||||
$app = new Http($swooleAdapter, 'UTC');
|
||||
$pools = $register->get('pools');
|
||||
Http::setResource('pools', fn () => $pools);
|
||||
$app = new Http('UTC');
|
||||
|
||||
/** @var Utopia\Database\Database $dbForPlatform */
|
||||
$dbForPlatform = $app->getResource('dbForPlatform');
|
||||
@@ -729,4 +726,4 @@ $http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
|
||||
});
|
||||
});
|
||||
|
||||
$swooleAdapter->start();
|
||||
$http->start();
|
||||
|
||||
+3
-12
@@ -106,12 +106,7 @@ use Appwrite\Utopia\Response\Model\Mock;
|
||||
use Appwrite\Utopia\Response\Model\MockNumber;
|
||||
use Appwrite\Utopia\Response\Model\None;
|
||||
use Appwrite\Utopia\Response\Model\Phone;
|
||||
use Appwrite\Utopia\Response\Model\PlatformAndroid;
|
||||
use Appwrite\Utopia\Response\Model\PlatformApple;
|
||||
use Appwrite\Utopia\Response\Model\PlatformLinux;
|
||||
use Appwrite\Utopia\Response\Model\PlatformList;
|
||||
use Appwrite\Utopia\Response\Model\PlatformWeb;
|
||||
use Appwrite\Utopia\Response\Model\PlatformWindows;
|
||||
use Appwrite\Utopia\Response\Model\Platform;
|
||||
use Appwrite\Utopia\Response\Model\Preferences;
|
||||
use Appwrite\Utopia\Response\Model\Project;
|
||||
use Appwrite\Utopia\Response\Model\Provider;
|
||||
@@ -202,6 +197,7 @@ Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, '
|
||||
Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, true));
|
||||
Response::setModel(new BaseList('Dev Keys List', Response::MODEL_DEV_KEY_LIST, 'devKeys', Response::MODEL_DEV_KEY, true, false));
|
||||
Response::setModel(new BaseList('Auth Providers List', Response::MODEL_AUTH_PROVIDER_LIST, 'platforms', Response::MODEL_AUTH_PROVIDER, true, false));
|
||||
Response::setModel(new BaseList('Platforms List', Response::MODEL_PLATFORM_LIST, 'platforms', Response::MODEL_PLATFORM, true, false));
|
||||
Response::setModel(new BaseList('Countries List', Response::MODEL_COUNTRY_LIST, 'countries', Response::MODEL_COUNTRY));
|
||||
Response::setModel(new BaseList('Continents List', Response::MODEL_CONTINENT_LIST, 'continents', Response::MODEL_CONTINENT));
|
||||
Response::setModel(new BaseList('Languages List', Response::MODEL_LANGUAGE_LIST, 'languages', Response::MODEL_LANGUAGE));
|
||||
@@ -337,12 +333,7 @@ Response::setModel(new Key());
|
||||
Response::setModel(new DevKey());
|
||||
Response::setModel(new MockNumber());
|
||||
Response::setModel(new AuthProvider());
|
||||
Response::setModel(new PlatformWeb());
|
||||
Response::setModel(new PlatformApple());
|
||||
Response::setModel(new PlatformAndroid());
|
||||
Response::setModel(new PlatformWindows());
|
||||
Response::setModel(new PlatformLinux());
|
||||
Response::setModel(new PlatformList());
|
||||
Response::setModel(new Platform());
|
||||
Response::setModel(new Variable());
|
||||
Response::setModel(new Country());
|
||||
Response::setModel(new Continent());
|
||||
|
||||
+13
-2
@@ -245,8 +245,19 @@ $register->set('pools', function () {
|
||||
$maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151);
|
||||
$instanceConnections = $maxConnections / (int) System::getEnv('_APP_POOL_CLIENTS', 14);
|
||||
|
||||
$workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
|
||||
$poolSize = max(1, (int)($instanceConnections / $workerCount));
|
||||
$multiprocessing = System::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled';
|
||||
|
||||
if ($multiprocessing) {
|
||||
$workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
|
||||
} else {
|
||||
$workerCount = 1;
|
||||
}
|
||||
|
||||
if ($workerCount > $instanceConnections) {
|
||||
throw new \Exception('Pool size is too small. Increase the number of allowed database connections or decrease the number of workers.', 500);
|
||||
}
|
||||
|
||||
$poolSize = (int)($instanceConnections / $workerCount);
|
||||
|
||||
foreach ($connections as $key => $connection) {
|
||||
$type = $connection['type'] ?? '';
|
||||
|
||||
+1372
-38
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,456 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Screenshot;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Utopia\Audit\Adapter\Database as AdapterDatabase;
|
||||
use Utopia\Audit\Audit as UtopiaAudit;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Adapter\Pool as DatabasePool;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DI\Container;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Registry\Registry;
|
||||
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter as Telemetry;
|
||||
|
||||
/**
|
||||
* Register per-job resources on the given container.
|
||||
* These resources depend on the queue message or keep mutable state and
|
||||
* must be fresh for each worker job.
|
||||
*/
|
||||
return function (Container $container): void {
|
||||
$container->set('log', fn () => new Log(), []);
|
||||
|
||||
$container->set('usage', fn () => new Context(), []);
|
||||
|
||||
$container->set('authorization', function () {
|
||||
$authorization = new Authorization();
|
||||
$authorization->disable();
|
||||
|
||||
return $authorization;
|
||||
}, []);
|
||||
|
||||
$container->set('dbForPlatform', function (Cache $cache, Group $pools, Authorization $authorization) {
|
||||
$adapter = new DatabasePool($pools->get('console'));
|
||||
$dbForPlatform = new Database($adapter, $cache);
|
||||
|
||||
$dbForPlatform
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setNamespace('_console')
|
||||
->setDocumentType('users', User::class);
|
||||
|
||||
return $dbForPlatform;
|
||||
}, ['cache', 'pools', 'authorization']);
|
||||
|
||||
$container->set('project', function ($message, Database $dbForPlatform) {
|
||||
$payload = $message->getPayload() ?? [];
|
||||
$project = new Document($payload['project'] ?? []);
|
||||
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $project;
|
||||
}
|
||||
|
||||
return $dbForPlatform->getDocument('projects', $project->getId());
|
||||
}, ['message', 'dbForPlatform']);
|
||||
|
||||
$container->set('dbForProject', function (Cache $cache, Group $pools, Document $project, Database $dbForPlatform, Authorization $authorization) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
}
|
||||
|
||||
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'));
|
||||
}
|
||||
|
||||
$adapter = new DatabasePool($pools->get($dsn->getHost()));
|
||||
$database = new Database($adapter, $cache);
|
||||
$database->setDocumentType('users', User::class);
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
|
||||
return $database;
|
||||
}, ['cache', 'pools', 'project', 'dbForPlatform', 'authorization']);
|
||||
|
||||
$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) {
|
||||
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
|
||||
|
||||
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
}
|
||||
|
||||
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'));
|
||||
}
|
||||
|
||||
if (isset($databases[$dsn->getHost()])) {
|
||||
$database = $databases[$dsn->getHost()];
|
||||
$database->setAuthorization($authorization);
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
return $database;
|
||||
}
|
||||
|
||||
$adapter = new DatabasePool($pools->get($dsn->getHost()));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
$databases[$dsn->getHost()] = $database;
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
|
||||
|
||||
$container->set('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) {
|
||||
return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database {
|
||||
$projectDocument ??= $project;
|
||||
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
|
||||
$databaseType = $database->getAttribute('type', '');
|
||||
|
||||
// Backwards-compatibility: older or seeded legacy databases may not have a DSN stored
|
||||
// in the "database" attribute. In that case, fall back to the project's database DSN.
|
||||
if ($databaseDSN === '') {
|
||||
$databaseDSN = $projectDocument->getAttribute('database', '');
|
||||
}
|
||||
|
||||
try {
|
||||
$databaseDSN = new DSN($databaseDSN);
|
||||
} catch (\InvalidArgumentException) {
|
||||
$databaseDSN = new DSN('mysql://' . $databaseDSN);
|
||||
}
|
||||
|
||||
try {
|
||||
$dsn = new DSN($projectDocument->getAttribute('database'));
|
||||
} catch (\InvalidArgumentException) {
|
||||
// Temporary fallback until all projects use shared tables
|
||||
$dsn = new DSN('mysql://' . $projectDocument->getAttribute('database'));
|
||||
}
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$databaseHost = $databaseDSN->getHost();
|
||||
$pool = $pools->get($databaseHost);
|
||||
|
||||
$adapter = new DatabasePool($pool);
|
||||
$database = new Database($adapter, $cache);
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization);
|
||||
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
|
||||
|
||||
$sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')));
|
||||
|
||||
// For separate pools (documentsdb/vectorsdb), check their own shared tables config.
|
||||
// If not configured, use dedicated mode to avoid cross-engine tenant type mismatches.
|
||||
if ($databaseHost !== $dsn->getHost()) {
|
||||
$dbTypeSharedTables = match ($databaseType) {
|
||||
DOCUMENTSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''))),
|
||||
VECTORSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''))),
|
||||
default => [],
|
||||
};
|
||||
|
||||
if (\in_array($databaseHost, $dbTypeSharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($projectDocument->getSequence())
|
||||
->setNamespace($databaseDSN->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $projectDocument->getSequence());
|
||||
}
|
||||
} elseif (\in_array($dsn->getHost(), $sharedTables, true)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($projectDocument->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $projectDocument->getSequence());
|
||||
}
|
||||
|
||||
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
return $database;
|
||||
};
|
||||
}, ['cache', 'register', 'project', 'authorization']);
|
||||
|
||||
$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
$database = null;
|
||||
|
||||
return function (?Document $project = null) use ($pools, $cache, $authorization, &$database) {
|
||||
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant($project->getSequence());
|
||||
|
||||
return $database;
|
||||
}
|
||||
|
||||
$adapter = new DatabasePool($pools->get('logs'));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setSharedTables(true)
|
||||
->setNamespace('logsV1')
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
|
||||
|
||||
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant($project->getSequence());
|
||||
}
|
||||
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
$container->set('abuseRetention', function () {
|
||||
return \time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day
|
||||
}, []);
|
||||
|
||||
$container->set('auditRetention', function (Document $project) {
|
||||
if ($project->getId() === 'console') {
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
|
||||
}
|
||||
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
|
||||
}, ['project']);
|
||||
|
||||
$container->set('executionRetention', function () {
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
|
||||
}, []);
|
||||
|
||||
$container->set('queueForDatabase', function (Publisher $publisher) {
|
||||
return new EventDatabase($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForMessaging', function (Publisher $publisher) {
|
||||
return new Messaging($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForMails', function (Publisher $publisher) {
|
||||
return new Mail($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForBuilds', function (Publisher $publisher) {
|
||||
return new Build($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForScreenshots', function (Publisher $publisher) {
|
||||
return new Screenshot($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForDeletes', function (Publisher $publisher) {
|
||||
return new Delete($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForEvents', function (Publisher $publisher) {
|
||||
return new Event($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForAudits', function (Publisher $publisher) {
|
||||
return new Audit($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForWebhooks', function (Publisher $publisher) {
|
||||
return new Webhook($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForFunctions', function (Publisher $publisher) {
|
||||
return new Func($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForRealtime', function () {
|
||||
return new Realtime();
|
||||
}, []);
|
||||
|
||||
$container->set('queueForCertificates', function (Publisher $publisher) {
|
||||
return new Certificate($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForMigrations', function (Publisher $publisher) {
|
||||
return new Migration($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('deviceForSites', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
$container->set('deviceForMigrations', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
$container->set('deviceForFunctions', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
$container->set('deviceForFiles', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
$container->set('deviceForBuilds', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
$container->set('deviceForCache', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
$container->set('logError', function (Registry $register, Document $project) {
|
||||
return function (Throwable $error, string $namespace, string $action, ?array $extras = null) 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());
|
||||
|
||||
if ($error->getPrevious() !== null) {
|
||||
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
|
||||
$log->addExtra('previousMessage', $error->getPrevious()->getMessage());
|
||||
}
|
||||
$log->addExtra('previousFile', $error->getPrevious()->getFile());
|
||||
$log->addExtra('previousLine', $error->getPrevious()->getLine());
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
if ($error->getPrevious() !== null) {
|
||||
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
|
||||
Console::warning("Previous Failed: {$error->getPrevious()->getMessage()}");
|
||||
}
|
||||
Console::warning("Previous File: {$error->getPrevious()->getFile()} Line: {$error->getPrevious()->getLine()}");
|
||||
}
|
||||
};
|
||||
}, ['register', 'project']);
|
||||
|
||||
$container->set('getAudit', function (Database $dbForPlatform, callable $getProjectDB) {
|
||||
return function (Document $project) use ($dbForPlatform, $getProjectDB) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
$adapter = new AdapterDatabase($dbForPlatform);
|
||||
|
||||
return new UtopiaAudit($adapter);
|
||||
}
|
||||
|
||||
$dbForProject = $getProjectDB($project);
|
||||
$adapter = new AdapterDatabase($dbForProject);
|
||||
|
||||
return new UtopiaAudit($adapter);
|
||||
};
|
||||
}, ['dbForPlatform', 'getProjectDB']);
|
||||
|
||||
$container->set('executionsRetentionCount', function (Document $project, array $plan) {
|
||||
if ($project->getId() === 'console' || empty($plan)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) ($plan['executionsRetentionCount'] ?? 100);
|
||||
}, ['project', 'plan']);
|
||||
};
|
||||
+24
-20
@@ -33,9 +33,7 @@ use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DI\Container;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Http\Adapter\FPM\Server as HttpServer;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Pools\Group;
|
||||
@@ -50,8 +48,6 @@ use Utopia\WebSocket\Server;
|
||||
*/
|
||||
require_once __DIR__ . '/init.php';
|
||||
|
||||
$registerRequestResources ??= require __DIR__ . '/init/resources/request.php';
|
||||
|
||||
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
|
||||
|
||||
// Log uncaught exceptions in one line instead of relying on Swoole's full backtrace dump
|
||||
@@ -244,11 +240,6 @@ if (!function_exists('triggerStats')) {
|
||||
}
|
||||
}
|
||||
|
||||
global $container;
|
||||
$container->set('pools', function ($register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
|
||||
$realtime = getRealtime();
|
||||
|
||||
/**
|
||||
@@ -392,6 +383,22 @@ $server->onStart(function () use ($stats, $containerId, &$statsDocument) {
|
||||
}
|
||||
});
|
||||
|
||||
function cloudRealtimeLogConnectionHostnames(Http $app, Document $project, Request $request): void
|
||||
{
|
||||
try {
|
||||
/** @var array<int, string> $allowed */
|
||||
$allowed = $app->getResource('allowedHostnames');
|
||||
Console::info(sprintf(
|
||||
'[Realtime] project=%s origin=%s allowedHostnames=%s',
|
||||
$project->getId(),
|
||||
$request->getOrigin(),
|
||||
json_encode(array_values($allowed))
|
||||
));
|
||||
} catch (Throwable $e) {
|
||||
Console::error('[Realtime] allowedHostnames log failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime) {
|
||||
Console::success('Worker ' . $workerId . ' started successfully');
|
||||
|
||||
@@ -623,22 +630,16 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
Console::error('Failed to restart pub/sub...');
|
||||
});
|
||||
|
||||
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $registerRequestResources) {
|
||||
global $container;
|
||||
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime) {
|
||||
$app = new Http('UTC');
|
||||
$request = new Request($request);
|
||||
$response = new Response(new SwooleResponse());
|
||||
|
||||
Console::info("Connection open (user: {$connection})");
|
||||
|
||||
$connectionContainer = new Container($container);
|
||||
|
||||
$adapter = new HttpServer($connectionContainer);
|
||||
$app = new Http($adapter, 'UTC');
|
||||
$connectionContainer->set('utopia', fn () => $app);
|
||||
$connectionContainer->set('request', fn () => $request);
|
||||
$connectionContainer->set('response', fn () => $response);
|
||||
|
||||
$registerRequestResources($connectionContainer);
|
||||
Http::setResource('pools', fn () => $register->get('pools'));
|
||||
Http::setResource('request', fn () => $request);
|
||||
Http::setResource('response', fn () => $response);
|
||||
|
||||
$project = null;
|
||||
$logUser = null;
|
||||
@@ -703,6 +704,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
*/
|
||||
$origin = $request->getOrigin();
|
||||
$originValidator = $app->getResource('originValidator');
|
||||
|
||||
|
||||
cloudRealtimeLogConnectionHostnames($app, $project, $request);
|
||||
|
||||
if (!empty($origin) && !$originValidator->isValid($origin) && $project->getId() !== 'console') {
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
|
||||
|
||||
+530
-42
@@ -1,69 +1,564 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/init.php';
|
||||
|
||||
$registerWorkerMessageResources = require __DIR__ . '/init/worker/message.php';
|
||||
use Appwrite\Certificates\LetsEncrypt;
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Screenshot;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Executor\Executor;
|
||||
use Swoole\Runtime;
|
||||
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
|
||||
use Utopia\Audit\Adapter\Database as AdapterDatabase;
|
||||
use Utopia\Audit\Audit as UtopiaAudit;
|
||||
use Utopia\Cache\Adapter\Pool as CachePool;
|
||||
use Utopia\Cache\Adapter\Sharding;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Adapter\Pool as DatabasePool;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Platform\Service;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Queue\Adapter\Swoole;
|
||||
use Utopia\Queue\Broker\Pool as BrokerPool;
|
||||
use Utopia\Queue\Message;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Queue\Queue;
|
||||
use Utopia\Queue\Server;
|
||||
use Utopia\Registry\Registry;
|
||||
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter as Telemetry;
|
||||
use Utopia\Telemetry\Adapter\None as NoTelemetry;
|
||||
|
||||
Runtime::enableCoroutine();
|
||||
require_once __DIR__ . '/init/span.php';
|
||||
|
||||
global $container;
|
||||
$container->set('pools', function ($register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
global $register;
|
||||
Server::setResource('register', fn () => $register);
|
||||
|
||||
$container->set('authorization', function () {
|
||||
Server::setResource('authorization', function () {
|
||||
$authorization = new Authorization();
|
||||
$authorization->disable();
|
||||
|
||||
return $authorization;
|
||||
}, []);
|
||||
|
||||
$container->set('project', fn () => new Document([]), []);
|
||||
Server::setResource('dbForPlatform', function (Cache $cache, Registry $register, Authorization $authorization) {
|
||||
$pools = $register->get('pools');
|
||||
$adapter = new DatabasePool($pools->get('console'));
|
||||
$dbForPlatform = new Database($adapter, $cache);
|
||||
|
||||
$container->set('log', fn () => new Log(), []);
|
||||
$dbForPlatform
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setNamespace('_console')
|
||||
->setDocumentType('users', User::class);
|
||||
|
||||
$container->set('consumer', function (Group $pools) {
|
||||
return $dbForPlatform;
|
||||
}, ['cache', 'register', 'authorization']);
|
||||
|
||||
Server::setResource('project', function (Message $message, Database $dbForPlatform) {
|
||||
$payload = $message->getPayload() ?? [];
|
||||
$project = new Document($payload['project'] ?? []);
|
||||
|
||||
if ($project->getId() === 'console') {
|
||||
return $project;
|
||||
}
|
||||
|
||||
return $dbForPlatform->getDocument('projects', $project->getId());
|
||||
}, ['message', 'dbForPlatform']);
|
||||
|
||||
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForPlatform, Authorization $authorization) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
}
|
||||
|
||||
$pools = $register->get('pools');
|
||||
|
||||
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'));
|
||||
}
|
||||
|
||||
$adapter = new DatabasePool($pools->get($dsn->getHost()));
|
||||
$database = new Database($adapter, $cache);
|
||||
$database->setDocumentType('users', User::class);
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
|
||||
return $database;
|
||||
}, ['cache', 'register', 'message', 'project', 'dbForPlatform', 'authorization']);
|
||||
|
||||
Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) {
|
||||
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
|
||||
|
||||
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
}
|
||||
|
||||
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'));
|
||||
}
|
||||
|
||||
if (isset($databases[$dsn->getHost()])) {
|
||||
$database = $databases[$dsn->getHost()];
|
||||
$database->setAuthorization($authorization);
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
return $database;
|
||||
}
|
||||
|
||||
$adapter = new DatabasePool($pools->get($dsn->getHost()));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
$databases[$dsn->getHost()] = $database;
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
|
||||
|
||||
Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
$database = null;
|
||||
|
||||
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
|
||||
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant($project->getSequence());
|
||||
return $database;
|
||||
}
|
||||
|
||||
$adapter = new DatabasePool($pools->get('logs'));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setSharedTables(true)
|
||||
->setNamespace('logsV1')
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
|
||||
|
||||
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant($project->getSequence());
|
||||
}
|
||||
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) {
|
||||
return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database {
|
||||
$projectDocument ??= $project;
|
||||
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
|
||||
$databaseType = $database->getAttribute('type', '');
|
||||
|
||||
// Backwards‑compatibility: older or seeded legacy databases may not have a DSN stored
|
||||
// in the "database" attribute. In that case, fall back to the project's database DSN.
|
||||
if ($databaseDSN === '') {
|
||||
$databaseDSN = $projectDocument->getAttribute('database', '');
|
||||
}
|
||||
|
||||
try {
|
||||
$databaseDSN = new DSN($databaseDSN);
|
||||
} catch (\InvalidArgumentException) {
|
||||
$databaseDSN = new DSN('mysql://'.$databaseDSN);
|
||||
}
|
||||
|
||||
try {
|
||||
$dsn = new DSN($projectDocument->getAttribute('database'));
|
||||
} catch (\InvalidArgumentException) {
|
||||
// Temporary fallback until all projects use shared tables
|
||||
$dsn = new DSN('mysql://' . $projectDocument->getAttribute('database'));
|
||||
}
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$pool = $pools->get($databaseDSN->getHost());
|
||||
|
||||
$adapter = new DatabasePool($pool);
|
||||
$database = new Database($adapter, $cache);
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization);
|
||||
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables, true)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant((int) $projectDocument->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $projectDocument->getSequence());
|
||||
}
|
||||
|
||||
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
return $database;
|
||||
};
|
||||
}, ['cache', 'register', 'project', 'authorization']);
|
||||
|
||||
Server::setResource('abuseRetention', function () {
|
||||
return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day
|
||||
});
|
||||
|
||||
Server::setResource('auditRetention', function (Document $project) {
|
||||
if ($project->getId() === 'console') {
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
|
||||
}
|
||||
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
|
||||
}, ['project']);
|
||||
|
||||
Server::setResource('executionRetention', function () {
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
|
||||
});
|
||||
|
||||
Server::setResource('cache', function (Registry $register) {
|
||||
$pools = $register->get('pools');
|
||||
$list = Config::getParam('pools-cache', []);
|
||||
$adapters = [];
|
||||
|
||||
foreach ($list as $value) {
|
||||
$adapters[] = new CachePool($pools->get($value));
|
||||
}
|
||||
|
||||
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('publisher', function (Group $pools) {
|
||||
return new BrokerPool(publisher: $pools->get('publisher'));
|
||||
}, ['pools']);
|
||||
|
||||
Server::setResource('publisherDatabases', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('publisherFunctions', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('publisherMigrations', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('publisherMessaging', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('consumer', function (Group $pools) {
|
||||
return new BrokerPool(consumer: $pools->get('consumer'));
|
||||
}, ['pools']);
|
||||
|
||||
$container->set('consumerDatabases', function (BrokerPool $consumer) {
|
||||
Server::setResource('consumerDatabases', function (BrokerPool $consumer) {
|
||||
return $consumer;
|
||||
}, ['consumer']);
|
||||
|
||||
$container->set('consumerMigrations', function (BrokerPool $consumer) {
|
||||
Server::setResource('consumerMigrations', function (BrokerPool $consumer) {
|
||||
return $consumer;
|
||||
}, ['consumer']);
|
||||
|
||||
$container->set('consumerStatsUsage', function (BrokerPool $consumer) {
|
||||
Server::setResource('consumerStatsUsage', function (BrokerPool $consumer) {
|
||||
return $consumer;
|
||||
}, ['consumer']);
|
||||
|
||||
$container->set('certificates', function () {
|
||||
Server::setResource('usage', function () {
|
||||
return new Context();
|
||||
}, []);
|
||||
Server::setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
|
||||
Server::setResource('queueForDatabase', function (Publisher $publisher) {
|
||||
return new EventDatabase($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForMessaging', function (Publisher $publisher) {
|
||||
return new Messaging($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForMails', function (Publisher $publisher) {
|
||||
return new Mail($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForBuilds', function (Publisher $publisher) {
|
||||
return new Build($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForScreenshots', function (Publisher $publisher) {
|
||||
return new Screenshot($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForDeletes', function (Publisher $publisher) {
|
||||
return new Delete($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForEvents', function (Publisher $publisher) {
|
||||
return new Event($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForAudits', function (Publisher $publisher) {
|
||||
return new Audit($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForWebhooks', function (Publisher $publisher) {
|
||||
return new Webhook($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForFunctions', function (Publisher $publisher) {
|
||||
return new Func($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForRealtime', function () {
|
||||
return new Realtime();
|
||||
}, []);
|
||||
|
||||
Server::setResource('queueForCertificates', function (Publisher $publisher) {
|
||||
return new Certificate($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForMigrations', function (Publisher $publisher) {
|
||||
return new Migration($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('logger', function (Registry $register) {
|
||||
return $register->get('logger');
|
||||
}, ['register']);
|
||||
|
||||
Server::setResource('pools', function (Registry $register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
|
||||
Server::setResource('telemetry', fn () => new NoTelemetry());
|
||||
|
||||
Server::setResource('deviceForSites', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
Server::setResource('deviceForMigrations', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
Server::setResource('deviceForFunctions', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
Server::setResource('deviceForFiles', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
Server::setResource('deviceForBuilds', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
Server::setResource('deviceForCache', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
|
||||
Server::setResource(
|
||||
'isResourceBlocked',
|
||||
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
|
||||
);
|
||||
|
||||
Server::setResource('plan', function (array $plan = []) {
|
||||
return [];
|
||||
});
|
||||
|
||||
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 = null) 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());
|
||||
|
||||
if ($error->getPrevious() !== null) {
|
||||
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
|
||||
$log->addExtra('previousMessage', $error->getPrevious()->getMessage());
|
||||
}
|
||||
$log->addExtra('previousFile', $error->getPrevious()->getFile());
|
||||
$log->addExtra('previousLine', $error->getPrevious()->getLine());
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
if ($error->getPrevious() !== null) {
|
||||
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
|
||||
Console::warning("Previous Failed: {$error->getPrevious()->getMessage()}");
|
||||
}
|
||||
Console::warning("Previous File: {$error->getPrevious()->getFile()} Line: {$error->getPrevious()->getLine()}");
|
||||
}
|
||||
};
|
||||
}, ['register', 'project']);
|
||||
|
||||
Server::setResource('executor', fn () => new Executor());
|
||||
|
||||
Server::setResource('getAudit', function (Database $dbForPlatform, callable $getProjectDB) {
|
||||
return function (Document $project) use ($dbForPlatform, $getProjectDB) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
$adapter = new AdapterDatabase($dbForPlatform);
|
||||
|
||||
return new UtopiaAudit($adapter);
|
||||
}
|
||||
|
||||
$dbForProject = $getProjectDB($project);
|
||||
$adapter = new AdapterDatabase($dbForProject);
|
||||
|
||||
return new UtopiaAudit($adapter);
|
||||
};
|
||||
}, ['dbForPlatform', 'getProjectDB']);
|
||||
|
||||
Server::setResource('executionsRetentionCount', function (Document $project, array $plan) {
|
||||
if ($project->getId() === 'console' || empty($plan)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) ($plan['executionsRetentionCount'] ?? 100);
|
||||
}, ['project', 'plan']);
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$platform = new Appwrite();
|
||||
$args = $_SERVER['argv'] ?? [];
|
||||
$args = $platform->getEnv('argv');
|
||||
|
||||
if (! isset($args[1])) {
|
||||
Console::error('Missing worker name');
|
||||
@@ -79,45 +574,38 @@ if (\str_starts_with($workerName, 'databases')) {
|
||||
$queueName = System::getEnv('_APP_QUEUE_NAME', 'v1-' . strtolower($workerName));
|
||||
}
|
||||
|
||||
/** @var \Utopia\Pools\Group $pools */
|
||||
$pools = $container->get('pools');
|
||||
|
||||
$adapter = new Swoole(
|
||||
$pools->get('consumer')->pop()->getResource(),
|
||||
System::getEnv('_APP_WORKERS_NUM', 1),
|
||||
$queueName
|
||||
);
|
||||
|
||||
$worker = new Server($adapter, $container);
|
||||
|
||||
try {
|
||||
$worker->init()->action(function () use ($worker, $registerWorkerMessageResources) {
|
||||
$registerWorkerMessageResources($worker->getContainer());
|
||||
});
|
||||
|
||||
$container->set('bus', function ($register) use ($worker) {
|
||||
return $register->get('bus')->setResolver(
|
||||
fn (string $name) => $worker->getContainer()->get($name)
|
||||
);
|
||||
}, ['register']);
|
||||
|
||||
$platform->setWorker($worker);
|
||||
/**
|
||||
* Any worker can be configured with the following env vars:
|
||||
* - _APP_WORKERS_NUM The total number of worker processes
|
||||
* - _APP_WORKER_PER_CORE The number of worker processes per core (ignored if _APP_WORKERS_NUM is set)
|
||||
* - _APP_QUEUE_NAME The name of the queue to read for database events
|
||||
*/
|
||||
$platform->init(Service::TYPE_WORKER, [
|
||||
'workerName' => strtolower($workerName),
|
||||
'workersNum' => System::getEnv('_APP_WORKERS_NUM', 1),
|
||||
'connection' => $pools->get('consumer')->pop()->getResource(),
|
||||
'workerName' => strtolower($workerName) ?? null,
|
||||
'queueName' => $queueName,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine());
|
||||
Console::exit(1);
|
||||
}
|
||||
|
||||
$worker = $platform->getWorker();
|
||||
|
||||
Server::setResource('bus', function ($register) use ($worker) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $worker->getResource($name));
|
||||
}, ['register']);
|
||||
|
||||
$worker
|
||||
->error()
|
||||
->inject('error')
|
||||
->inject('logger')
|
||||
->inject('log')
|
||||
->inject('pools')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->action(function (Throwable $error, ?Logger $logger, Log $log, Document $project, Authorization $authorization) use ($queueName) {
|
||||
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project, Authorization $authorization) use ($queueName) {
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
if ($logger) {
|
||||
|
||||
+8
-7
@@ -52,34 +52,34 @@
|
||||
"appwrite/php-runtimes": "0.19.*",
|
||||
"appwrite/php-clamav": "2.0.*",
|
||||
"utopia-php/abuse": "1.2.*",
|
||||
"utopia-php/agents": "1.2.*",
|
||||
"utopia-php/analytics": "0.15.*",
|
||||
"utopia-php/audit": "2.2.*",
|
||||
"utopia-php/auth": "0.5.*",
|
||||
"utopia-php/cache": "1.0.*",
|
||||
"utopia-php/cli": "0.23.*",
|
||||
"utopia-php/cli": "0.22.*",
|
||||
"utopia-php/compression": "0.1.*",
|
||||
"utopia-php/config": "1.*",
|
||||
"utopia-php/console": "0.1.*",
|
||||
"utopia-php/database": "5.*",
|
||||
"utopia-php/agents": "1.*",
|
||||
"utopia-php/detector": "0.2.*",
|
||||
"utopia-php/domains": "1.*",
|
||||
"utopia-php/emails": "0.6.*",
|
||||
"utopia-php/dns": "1.6.*",
|
||||
"utopia-php/dsn": "0.2.1",
|
||||
"utopia-php/http": "0.34.*",
|
||||
"utopia-php/framework": "0.33.*",
|
||||
"utopia-php/fetch": "0.5.*",
|
||||
"utopia-php/image": "0.8.*",
|
||||
"utopia-php/locale": "0.8.*",
|
||||
"utopia-php/logger": "0.6.*",
|
||||
"utopia-php/messaging": "0.22.*",
|
||||
"utopia-php/migration": "1.9.*",
|
||||
"utopia-php/platform": "0.12.*",
|
||||
"utopia-php/platform": "0.7.*",
|
||||
"utopia-php/pools": "1.*",
|
||||
"utopia-php/span": "1.1.*",
|
||||
"utopia-php/preloader": "0.2.*",
|
||||
"utopia-php/queue": "0.17.*",
|
||||
"utopia-php/servers": "0.3.*",
|
||||
"utopia-php/queue": "0.15.*",
|
||||
"utopia-php/servers": "0.2.5",
|
||||
"utopia-php/registry": "0.5.*",
|
||||
"utopia-php/storage": "1.0.*",
|
||||
"utopia-php/system": "0.10.*",
|
||||
@@ -94,7 +94,8 @@
|
||||
"spomky-labs/otphp": "11.*",
|
||||
"webonyx/graphql-php": "14.11.*",
|
||||
"league/csv": "9.14.*",
|
||||
"enshrined/svg-sanitize": "0.22.*"
|
||||
"enshrined/svg-sanitize": "0.22.*",
|
||||
"utopia-php/di": "0.1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-fileinfo": "*",
|
||||
|
||||
Generated
+146
-139
@@ -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": "4fb974e9843f6104e40396e7cad4a833",
|
||||
"content-hash": "4fe91e67f343fbe6deac1fdc7eda949f",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -2708,16 +2708,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-client",
|
||||
"version": "v7.4.8",
|
||||
"version": "v7.4.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-client.git",
|
||||
"reference": "01933e626c3de76bea1e22641e205e78f6a34342"
|
||||
"reference": "1010624285470eb60e88ed10035102c75b4ea6af"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-client/zipball/01933e626c3de76bea1e22641e205e78f6a34342",
|
||||
"reference": "01933e626c3de76bea1e22641e205e78f6a34342",
|
||||
"url": "https://api.github.com/repos/symfony/http-client/zipball/1010624285470eb60e88ed10035102c75b4ea6af",
|
||||
"reference": "1010624285470eb60e88ed10035102c75b4ea6af",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2785,7 +2785,7 @@
|
||||
"http"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/http-client/tree/v7.4.8"
|
||||
"source": "https://github.com/symfony/http-client/tree/v7.4.7"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2805,7 +2805,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-30T12:55:43+00:00"
|
||||
"time": "2026-03-05T11:16:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-client-contracts",
|
||||
@@ -3403,16 +3403,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/agents",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/agents.git",
|
||||
"reference": "052227953678a30ecc4b5467401fcb0b2386471e"
|
||||
"reference": "06064fd9fb19b77ae45a12ec7bcbc17670912c30"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/agents/zipball/052227953678a30ecc4b5467401fcb0b2386471e",
|
||||
"reference": "052227953678a30ecc4b5467401fcb0b2386471e",
|
||||
"url": "https://api.github.com/repos/utopia-php/agents/zipball/06064fd9fb19b77ae45a12ec7bcbc17670912c30",
|
||||
"reference": "06064fd9fb19b77ae45a12ec7bcbc17670912c30",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3450,9 +3450,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/agents/issues",
|
||||
"source": "https://github.com/utopia-php/agents/tree/1.2.1"
|
||||
"source": "https://github.com/utopia-php/agents/tree/1.3.0"
|
||||
},
|
||||
"time": "2026-02-24T06:03:55+00:00"
|
||||
"time": "2026-03-26T03:51:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/analytics",
|
||||
@@ -3658,21 +3658,21 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/cli",
|
||||
"version": "0.23.1",
|
||||
"version": "0.22.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/cli.git",
|
||||
"reference": "8d1955b8bc4dc631f45d7c7df689ed7b63f70621"
|
||||
"reference": "a7ac387ee626fd27075a87e836fb72c5be38add4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/cli/zipball/8d1955b8bc4dc631f45d7c7df689ed7b63f70621",
|
||||
"reference": "8d1955b8bc4dc631f45d7c7df689ed7b63f70621",
|
||||
"url": "https://api.github.com/repos/utopia-php/cli/zipball/a7ac387ee626fd27075a87e836fb72c5be38add4",
|
||||
"reference": "a7ac387ee626fd27075a87e836fb72c5be38add4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.4",
|
||||
"utopia-php/servers": "0.3.*"
|
||||
"utopia-php/servers": "0.2.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.2.*",
|
||||
@@ -3703,9 +3703,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/cli/issues",
|
||||
"source": "https://github.com/utopia-php/cli/tree/0.23.1"
|
||||
"source": "https://github.com/utopia-php/cli/tree/0.22.0"
|
||||
},
|
||||
"time": "2026-04-05T15:27:35+00:00"
|
||||
"time": "2025-10-21T10:42:45+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/compression",
|
||||
@@ -3850,16 +3850,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/database",
|
||||
"version": "5.3.19",
|
||||
"version": "5.3.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/database.git",
|
||||
"reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691"
|
||||
"reference": "cff2b6ed63d3291b74110d086e16ff089fe05993"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691",
|
||||
"reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/cff2b6ed63d3291b74110d086e16ff089fe05993",
|
||||
"reference": "cff2b6ed63d3291b74110d086e16ff089fe05993",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3903,9 +3903,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/database/issues",
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.19"
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.17"
|
||||
},
|
||||
"time": "2026-03-31T15:52:08+00:00"
|
||||
"time": "2026-03-20T01:18:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/detector",
|
||||
@@ -3954,26 +3954,25 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/di",
|
||||
"version": "0.3.2",
|
||||
"version": "0.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/di.git",
|
||||
"reference": "07025d721ed5d9be27932e8e640acf1467fc4b9d"
|
||||
"reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/di/zipball/07025d721ed5d9be27932e8e640acf1467fc4b9d",
|
||||
"reference": "07025d721ed5d9be27932e8e640acf1467fc4b9d",
|
||||
"url": "https://api.github.com/repos/utopia-php/di/zipball/22490c95f7ac3898ed1c33f1b1b5dd577305ee31",
|
||||
"reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"psr/container": "^2.0"
|
||||
"php": ">=8.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "^1.27",
|
||||
"laravel/pint": "^1.2",
|
||||
"phpbench/phpbench": "^1.2",
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpunit/phpunit": "^9.5.25",
|
||||
"swoole/ide-helper": "4.8.3"
|
||||
},
|
||||
@@ -3990,18 +3989,16 @@
|
||||
],
|
||||
"description": "A simple and lite library for managing dependency injections",
|
||||
"keywords": [
|
||||
"PSR-11",
|
||||
"container",
|
||||
"dependency-injection",
|
||||
"di",
|
||||
"framework",
|
||||
"http",
|
||||
"php",
|
||||
"utopia"
|
||||
"upf"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/di/issues",
|
||||
"source": "https://github.com/utopia-php/di/tree/0.3.2"
|
||||
"source": "https://github.com/utopia-php/di/tree/0.1.0"
|
||||
},
|
||||
"time": "2026-03-21T07:42:10+00:00"
|
||||
"time": "2024-08-08T14:35:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/dns",
|
||||
@@ -4270,35 +4267,31 @@
|
||||
"time": "2025-12-18T16:25:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/http",
|
||||
"version": "0.34.19",
|
||||
"name": "utopia-php/framework",
|
||||
"version": "0.33.41",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/http.git",
|
||||
"reference": "995c119f31866cacd42d63b1f922bf86eabb396c"
|
||||
"reference": "0f3bf2377c867e547c929c3733b8224afee6ef06"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/995c119f31866cacd42d63b1f922bf86eabb396c",
|
||||
"reference": "995c119f31866cacd42d63b1f922bf86eabb396c",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/0f3bf2377c867e547c929c3733b8224afee6ef06",
|
||||
"reference": "0f3bf2377c867e547c929c3733b8224afee6ef06",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-swoole": "*",
|
||||
"php": ">=8.2",
|
||||
"php": ">=8.3",
|
||||
"utopia-php/compression": "0.1.*",
|
||||
"utopia-php/di": "0.3.*",
|
||||
"utopia-php/servers": "0.3.*",
|
||||
"utopia-php/telemetry": "0.2.*",
|
||||
"utopia-php/validators": "0.2.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/instantiator": "^1.5",
|
||||
"laravel/pint": "1.*",
|
||||
"phpbench/phpbench": "^1.2",
|
||||
"phpbench/phpbench": "1.*",
|
||||
"phpstan/phpstan": "1.*",
|
||||
"phpunit/phpunit": "^9.5.25",
|
||||
"swoole/ide-helper": "4.8.3"
|
||||
"phpunit/phpunit": "9.*",
|
||||
"swoole/ide-helper": "^6.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
@@ -4310,18 +4303,17 @@
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "A simple, light and advanced PHP HTTP framework",
|
||||
"description": "A simple, light and advanced PHP framework",
|
||||
"keywords": [
|
||||
"framework",
|
||||
"http",
|
||||
"php",
|
||||
"upf"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/http/issues",
|
||||
"source": "https://github.com/utopia-php/http/tree/0.34.19"
|
||||
"source": "https://github.com/utopia-php/http/tree/0.33.41"
|
||||
},
|
||||
"time": "2026-04-08T10:23:17+00:00"
|
||||
"time": "2026-02-24T12:01:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/image",
|
||||
@@ -4642,30 +4634,30 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/platform",
|
||||
"version": "0.12.1",
|
||||
"version": "0.7.16",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/platform.git",
|
||||
"reference": "2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc"
|
||||
"reference": "34e67e4b80b5741c380071fe765fbc12a132de4f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/platform/zipball/2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc",
|
||||
"reference": "2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc",
|
||||
"url": "https://api.github.com/repos/utopia-php/platform/zipball/34e67e4b80b5741c380071fe765fbc12a132de4f",
|
||||
"reference": "34e67e4b80b5741c380071fe765fbc12a132de4f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"ext-redis": "*",
|
||||
"php": ">=8.1",
|
||||
"utopia-php/cli": "0.23.*",
|
||||
"utopia-php/http": "0.34.*",
|
||||
"utopia-php/queue": "0.17.*",
|
||||
"utopia-php/servers": "0.3.*"
|
||||
"php": ">=8.0",
|
||||
"utopia-php/cli": "0.22.*",
|
||||
"utopia-php/framework": "0.33.*",
|
||||
"utopia-php/queue": "0.15.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.2.*",
|
||||
"phpunit/phpunit": "^9.3"
|
||||
"laravel/pint": "1.*",
|
||||
"phpstan/phpstan": "2.*",
|
||||
"phpunit/phpunit": "9.*"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
@@ -4687,9 +4679,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/platform/issues",
|
||||
"source": "https://github.com/utopia-php/platform/tree/0.12.1"
|
||||
"source": "https://github.com/utopia-php/platform/tree/0.7.16"
|
||||
},
|
||||
"time": "2026-04-08T04:11:31+00:00"
|
||||
"time": "2026-02-11T06:36:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/pools",
|
||||
@@ -4799,33 +4791,32 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/queue",
|
||||
"version": "0.17.0",
|
||||
"version": "0.15.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/queue.git",
|
||||
"reference": "0fbc7d7312f5cf76ec112513fb93317000901f5f"
|
||||
"reference": "08e361d69610f371382b344c369eef355ca414b4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/queue/zipball/0fbc7d7312f5cf76ec112513fb93317000901f5f",
|
||||
"reference": "0fbc7d7312f5cf76ec112513fb93317000901f5f",
|
||||
"url": "https://api.github.com/repos/utopia-php/queue/zipball/08e361d69610f371382b344c369eef355ca414b4",
|
||||
"reference": "08e361d69610f371382b344c369eef355ca414b4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.3",
|
||||
"php-amqplib/php-amqplib": "^3.7",
|
||||
"utopia-php/di": "0.3.*",
|
||||
"utopia-php/fetch": "0.5.*",
|
||||
"utopia-php/pools": "1.*",
|
||||
"utopia-php/servers": "0.3.*",
|
||||
"utopia-php/servers": "0.2.*",
|
||||
"utopia-php/telemetry": "0.2.*",
|
||||
"utopia-php/validators": "0.2.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-redis": "*",
|
||||
"laravel/pint": "^1.0",
|
||||
"laravel/pint": "^0.2.3",
|
||||
"phpstan/phpstan": "^1.8",
|
||||
"phpunit/phpunit": "^11.0",
|
||||
"phpunit/phpunit": "^9.5.5",
|
||||
"swoole/ide-helper": "4.8.8",
|
||||
"workerman/workerman": "^4.0"
|
||||
},
|
||||
@@ -4860,9 +4851,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/queue/issues",
|
||||
"source": "https://github.com/utopia-php/queue/tree/0.17.0"
|
||||
"source": "https://github.com/utopia-php/queue/tree/0.15.6"
|
||||
},
|
||||
"time": "2026-03-23T16:21:31+00:00"
|
||||
"time": "2026-02-23T13:03:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/registry",
|
||||
@@ -4918,21 +4909,21 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/servers",
|
||||
"version": "0.3.0",
|
||||
"version": "0.2.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/servers.git",
|
||||
"reference": "235be31200df9437fc96a1c270ffef4c64fafe52"
|
||||
"reference": "4770e879a90685af4ba14e7e5d95d0a17c7fdf03"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/servers/zipball/235be31200df9437fc96a1c270ffef4c64fafe52",
|
||||
"reference": "235be31200df9437fc96a1c270ffef4c64fafe52",
|
||||
"url": "https://api.github.com/repos/utopia-php/servers/zipball/4770e879a90685af4ba14e7e5d95d0a17c7fdf03",
|
||||
"reference": "4770e879a90685af4ba14e7e5d95d0a17c7fdf03",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"utopia-php/di": "0.3.*",
|
||||
"php": ">=8.0",
|
||||
"utopia-php/di": "0.1.*",
|
||||
"utopia-php/validators": "0.*"
|
||||
},
|
||||
"require-dev": {
|
||||
@@ -4966,9 +4957,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/servers/issues",
|
||||
"source": "https://github.com/utopia-php/servers/tree/0.3.0"
|
||||
"source": "https://github.com/utopia-php/servers/tree/0.2.5"
|
||||
},
|
||||
"time": "2026-03-13T11:31:42+00:00"
|
||||
"time": "2026-02-10T04:21:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/span",
|
||||
@@ -5448,16 +5439,16 @@
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "1.17.7",
|
||||
"version": "1.14.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "291471d04c3f0e7b9fcc46668a6255a4c0f2947e"
|
||||
"reference": "7e7e257b10a8c1384a237e7d8d73452e2108901e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/291471d04c3f0e7b9fcc46668a6255a4c0f2947e",
|
||||
"reference": "291471d04c3f0e7b9fcc46668a6255a4c0f2947e",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/7e7e257b10a8c1384a237e7d8d73452e2108901e",
|
||||
"reference": "7e7e257b10a8c1384a237e7d8d73452e2108901e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5493,22 +5484,22 @@
|
||||
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/sdk-generator/issues",
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.17.7"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.14.0"
|
||||
},
|
||||
"time": "2026-04-08T08:51:05+00:00"
|
||||
"time": "2026-03-26T12:50:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brianium/paratest",
|
||||
"version": "v7.20.0",
|
||||
"version": "v7.19.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/paratestphp/paratest.git",
|
||||
"reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d"
|
||||
"reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/81c80677c9ec0ed4ef16b246167f11dec81a6e3d",
|
||||
"reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d",
|
||||
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9",
|
||||
"reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5532,7 +5523,7 @@
|
||||
"ext-pcntl": "*",
|
||||
"ext-pcov": "*",
|
||||
"ext-posix": "*",
|
||||
"phpstan/phpstan": "^2.1.44",
|
||||
"phpstan/phpstan": "^2.1.40",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0.4",
|
||||
"phpstan/phpstan-phpunit": "^2.0.16",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.10",
|
||||
@@ -5576,7 +5567,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/paratestphp/paratest/issues",
|
||||
"source": "https://github.com/paratestphp/paratest/tree/v7.20.0"
|
||||
"source": "https://github.com/paratestphp/paratest/tree/v7.19.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5588,7 +5579,7 @@
|
||||
"type": "paypal"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-29T15:46:14+00:00"
|
||||
"time": "2026-03-09T14:33:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "czproject/git-php",
|
||||
@@ -6204,11 +6195,11 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.1.46",
|
||||
"version": "2.1.44",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/a193923fc2d6325ef4e741cf3af8c3e8f54dbf25",
|
||||
"reference": "a193923fc2d6325ef4e741cf3af8c3e8f54dbf25",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/4a88c083c668b2c364a425c9b3171b2d9ea5d218",
|
||||
"reference": "4a88c083c668b2c364a425c9b3171b2d9ea5d218",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6253,7 +6244,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-01T09:25:14+00:00"
|
||||
"time": "2026-03-25T17:34:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
@@ -6603,16 +6594,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "12.5.17",
|
||||
"version": "12.5.14",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "85b62adab1a340982df64e66daa4a4435eb5723b"
|
||||
"reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/85b62adab1a340982df64e66daa4a4435eb5723b",
|
||||
"reference": "85b62adab1a340982df64e66daa4a4435eb5723b",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/47283cfd98d553edcb1353591f4e255dc1bb61f0",
|
||||
"reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6634,7 +6625,7 @@
|
||||
"sebastian/cli-parser": "^4.2.0",
|
||||
"sebastian/comparator": "^7.1.4",
|
||||
"sebastian/diff": "^7.0.0",
|
||||
"sebastian/environment": "^8.0.4",
|
||||
"sebastian/environment": "^8.0.3",
|
||||
"sebastian/exporter": "^7.0.2",
|
||||
"sebastian/global-state": "^8.0.2",
|
||||
"sebastian/object-enumerator": "^7.0.0",
|
||||
@@ -6681,15 +6672,31 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.17"
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.14"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://phpunit.de/sponsoring.html",
|
||||
"type": "other"
|
||||
"url": "https://phpunit.de/sponsors.html",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://liberapay.com/sebastianbergmann",
|
||||
"type": "liberapay"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||
"type": "thanks_dev"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-08T03:04:19+00:00"
|
||||
"time": "2026-02-18T12:38:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
@@ -6762,16 +6769,16 @@
|
||||
},
|
||||
{
|
||||
"name": "sebastian/comparator",
|
||||
"version": "7.1.5",
|
||||
"version": "7.1.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/comparator.git",
|
||||
"reference": "c284f55811f43d555e51e8e5c166ac40d3e33c63"
|
||||
"reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c284f55811f43d555e51e8e5c166ac40d3e33c63",
|
||||
"reference": "c284f55811f43d555e51e8e5c166ac40d3e33c63",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a7de5df2e094f9a80b40a522391a7e6022df5f6",
|
||||
"reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6830,7 +6837,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/comparator/issues",
|
||||
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/comparator/tree/7.1.5"
|
||||
"source": "https://github.com/sebastianbergmann/comparator/tree/7.1.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6850,7 +6857,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-08T04:43:00+00:00"
|
||||
"time": "2026-01-24T09:28:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/complexity",
|
||||
@@ -7674,16 +7681,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v8.0.8",
|
||||
"version": "v8.0.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7"
|
||||
"reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/5b66d385dc58f69652e56f78a4184615e3f2b7f7",
|
||||
"reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a",
|
||||
"reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7740,7 +7747,7 @@
|
||||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v8.0.8"
|
||||
"source": "https://github.com/symfony/console/tree/v8.0.7"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7760,7 +7767,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
"time": "2026-03-06T14:06:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
@@ -8094,16 +8101,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v8.0.8",
|
||||
"version": "v8.0.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc"
|
||||
"reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc",
|
||||
"reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674",
|
||||
"reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -8135,7 +8142,7 @@
|
||||
"description": "Executes commands in sub-processes",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/process/tree/v8.0.8"
|
||||
"source": "https://github.com/symfony/process/tree/v8.0.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8155,20 +8162,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
"time": "2026-01-26T15:08:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/string",
|
||||
"version": "v8.0.8",
|
||||
"version": "v8.0.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/string.git",
|
||||
"reference": "ae9488f874d7603f9d2dfbf120203882b645d963"
|
||||
"reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/ae9488f874d7603f9d2dfbf120203882b645d963",
|
||||
"reference": "ae9488f874d7603f9d2dfbf120203882b645d963",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
|
||||
"reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -8225,7 +8232,7 @@
|
||||
"utf8"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/string/tree/v8.0.8"
|
||||
"source": "https://github.com/symfony/string/tree/v8.0.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8245,7 +8252,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
"time": "2026-02-09T10:14:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "textalk/websocket",
|
||||
|
||||
@@ -1,320 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Auth\OAuth2;
|
||||
|
||||
use Appwrite\Auth\OAuth2;
|
||||
use Appwrite\OpenSSL\OpenSSL;
|
||||
use Utopia\System\System;
|
||||
|
||||
// Reference Material
|
||||
// https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code
|
||||
// https://docs.x.com/x-api/users/get-me
|
||||
|
||||
class X extends OAuth2
|
||||
{
|
||||
private const PKCE_STATE_KEY = '_pkce';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $user = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $tokens = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $scopes = [
|
||||
'tweet.read',
|
||||
'users.read',
|
||||
'users.email',
|
||||
'offline.access',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private string $pkceVerifier = '';
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'x';
|
||||
}
|
||||
|
||||
public function getLoginURL(): string
|
||||
{
|
||||
$state = $this->state;
|
||||
$state[self::PKCE_STATE_KEY] = $this->encryptPKCEVerifier($this->getPKCEVerifier());
|
||||
|
||||
return 'https://x.com/i/oauth2/authorize?' . \http_build_query([
|
||||
'response_type' => 'code',
|
||||
'client_id' => $this->appID,
|
||||
'redirect_uri' => $this->callback,
|
||||
'scope' => \implode(' ', $this->getScopes()),
|
||||
'state' => $this->base64UrlEncode(\json_encode($state, JSON_THROW_ON_ERROR)),
|
||||
'code_challenge' => $this->getPKCEChallenge(),
|
||||
'code_challenge_method' => 'S256',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $code
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getTokens(string $code): array
|
||||
{
|
||||
if (empty($this->tokens)) {
|
||||
$this->tokens = $this->decodeJsonObject($this->request(
|
||||
'POST',
|
||||
'https://api.x.com/2/oauth2/token',
|
||||
$this->tokenEndpointHeaders(),
|
||||
\http_build_query([
|
||||
'code' => $code,
|
||||
'client_id' => $this->appID,
|
||||
'grant_type' => 'authorization_code',
|
||||
'redirect_uri' => $this->callback,
|
||||
'code_verifier' => $this->getPKCEVerifier(),
|
||||
])
|
||||
));
|
||||
}
|
||||
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $refreshToken
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function refreshTokens(string $refreshToken): array
|
||||
{
|
||||
$this->tokens = $this->decodeJsonObject($this->request(
|
||||
'POST',
|
||||
'https://api.x.com/2/oauth2/token',
|
||||
$this->tokenEndpointHeaders(),
|
||||
\http_build_query([
|
||||
'client_id' => $this->appID,
|
||||
'refresh_token' => $refreshToken,
|
||||
'grant_type' => 'refresh_token',
|
||||
])
|
||||
));
|
||||
|
||||
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['data']['id'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserEmail(string $accessToken): string
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
return $user['data']['confirmed_email'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the OAuth email is verified.
|
||||
*
|
||||
* X returns a confirmed email only when the app has email access enabled
|
||||
* and the authenticated user has a confirmed email address.
|
||||
*
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEmailVerified(string $accessToken): bool
|
||||
{
|
||||
return !empty($this->getUserEmail($accessToken));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserName(string $accessToken): string
|
||||
{
|
||||
$user = $this->getUser($accessToken);
|
||||
|
||||
return $user['data']['name'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getUser(string $accessToken): array
|
||||
{
|
||||
if (empty($this->user)) {
|
||||
$this->user = $this->decodeJsonObject($this->request(
|
||||
'GET',
|
||||
'https://api.x.com/2/users/me?user.fields=confirmed_email',
|
||||
['Authorization: Bearer ' . $accessToken]
|
||||
));
|
||||
}
|
||||
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function parseState(string $state): ?array
|
||||
{
|
||||
$decoded = $this->base64UrlDecode($state);
|
||||
if ($decoded === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parsed = \json_decode($decoded, true);
|
||||
|
||||
if (!\is_array($parsed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$pkce = $parsed[self::PKCE_STATE_KEY] ?? null;
|
||||
|
||||
if (\is_array($pkce)) {
|
||||
$this->pkceVerifier = $this->decryptPKCEVerifier($pkce);
|
||||
}
|
||||
|
||||
unset($parsed[self::PKCE_STATE_KEY]);
|
||||
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function tokenEndpointHeaders(): array
|
||||
{
|
||||
return [
|
||||
'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret),
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function decodeJsonObject(string $json): array
|
||||
{
|
||||
$decoded = \json_decode($json, true);
|
||||
|
||||
return \is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
private function getPKCEVerifier(): string
|
||||
{
|
||||
if ($this->pkceVerifier === '') {
|
||||
$this->pkceVerifier = $this->base64UrlEncode(\random_bytes(64));
|
||||
}
|
||||
|
||||
return $this->pkceVerifier;
|
||||
}
|
||||
|
||||
private function getPKCEChallenge(): string
|
||||
{
|
||||
return $this->base64UrlEncode(\hash('sha256', $this->getPKCEVerifier(), true));
|
||||
}
|
||||
|
||||
private function encryptPKCEVerifier(string $verifier): array
|
||||
{
|
||||
$iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM));
|
||||
$key = $this->getPKCEStateKey();
|
||||
$tag = null;
|
||||
|
||||
$data = OpenSSL::encrypt($verifier, OpenSSL::CIPHER_AES_128_GCM, $key, OPENSSL_RAW_DATA, $iv, $tag);
|
||||
|
||||
if ($data === false || $tag === null) {
|
||||
throw new \Exception('Failed to encrypt PKCE verifier.');
|
||||
}
|
||||
|
||||
return [
|
||||
'data' => $this->base64UrlEncode($data),
|
||||
'iv' => \bin2hex($iv),
|
||||
'tag' => \bin2hex($tag),
|
||||
];
|
||||
}
|
||||
|
||||
private function decryptPKCEVerifier(array $payload): string
|
||||
{
|
||||
$data = $payload['data'] ?? '';
|
||||
$iv = $payload['iv'] ?? '';
|
||||
$tag = $payload['tag'] ?? '';
|
||||
|
||||
if ($data === '' || $iv === '' || $tag === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$decodedData = $this->base64UrlDecode($data);
|
||||
$decodedIv = \hex2bin($iv);
|
||||
$decodedTag = \hex2bin($tag);
|
||||
|
||||
if ($decodedData === false || $decodedIv === false || $decodedTag === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return OpenSSL::decrypt(
|
||||
$decodedData,
|
||||
OpenSSL::CIPHER_AES_128_GCM,
|
||||
$this->getPKCEStateKey(),
|
||||
OPENSSL_RAW_DATA,
|
||||
$decodedIv,
|
||||
$decodedTag
|
||||
) ?: '';
|
||||
}
|
||||
|
||||
private function getPKCEStateKey(): string
|
||||
{
|
||||
$key = System::getEnv('_APP_OPENSSL_KEY_V1', '');
|
||||
|
||||
if ($key === '') {
|
||||
throw new \Exception('X OAuth2 requires _APP_OPENSSL_KEY_V1 to encrypt PKCE state.');
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
private function base64UrlEncode(string $value): string
|
||||
{
|
||||
return \rtrim(\strtr(\base64_encode($value), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
private function base64UrlDecode(string $value): string|false
|
||||
{
|
||||
$padding = \strlen($value) % 4;
|
||||
if ($padding > 0) {
|
||||
$value .= \str_repeat('=', 4 - $padding);
|
||||
}
|
||||
|
||||
return \base64_decode(\strtr($value, '-_', '+/'), true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -333,8 +333,6 @@ class Exception extends \Exception
|
||||
|
||||
/** Platform */
|
||||
public const string PLATFORM_NOT_FOUND = 'platform_not_found';
|
||||
public const string PLATFORM_METHOD_UNSUPPORTED = 'platform_method_unsupported';
|
||||
public const string PLATFORM_ALREADY_EXISTS = 'platform_already_exists';
|
||||
|
||||
/** GraphqQL */
|
||||
public const string GRAPHQL_NO_QUERY = 'graphql_no_query';
|
||||
|
||||
@@ -26,9 +26,9 @@ class Resolvers
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $route, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
|
||||
$path = $route->getPath();
|
||||
foreach ($args as $key => $value) {
|
||||
@@ -93,9 +93,9 @@ class Resolvers
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
|
||||
$request->setMethod('GET');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
@@ -124,9 +124,9 @@ class Resolvers
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
|
||||
$request->setMethod('GET');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
@@ -160,9 +160,9 @@ class Resolvers
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
|
||||
$request->setMethod('POST');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
@@ -192,9 +192,9 @@ class Resolvers
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
|
||||
$request->setMethod('PATCH');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
@@ -222,9 +222,9 @@ class Resolvers
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql');
|
||||
$request = $utopia->getResource('request');
|
||||
$response = $utopia->getResource('response');
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
|
||||
$request->setMethod('DELETE');
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
@@ -266,7 +266,7 @@ class Resolvers
|
||||
try {
|
||||
$route = $utopia->match($request, fresh: true);
|
||||
|
||||
$utopia->execute($route, $request);
|
||||
$utopia->execute($route, $request, $response);
|
||||
} catch (\Throwable $e) {
|
||||
if ($beforeReject) {
|
||||
$e = $beforeReject($e);
|
||||
|
||||
@@ -32,6 +32,10 @@ class Schema
|
||||
array $urls,
|
||||
array $params,
|
||||
): GQLSchema {
|
||||
Http::setResource('utopia:graphql', static function () use ($utopia) {
|
||||
return $utopia;
|
||||
});
|
||||
|
||||
if (!empty(self::$schema)) {
|
||||
return self::$schema;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,20 @@ class Platform
|
||||
{
|
||||
public const TYPE_UNKNOWN = 'unknown';
|
||||
public const TYPE_WEB = 'web';
|
||||
public const TYPE_APPLE = 'apple';
|
||||
public const TYPE_FLUTTER_IOS = 'flutter-ios';
|
||||
public const TYPE_FLUTTER_ANDROID = 'flutter-android';
|
||||
public const TYPE_FLUTTER_MACOS = 'flutter-macos';
|
||||
public const TYPE_FLUTTER_WINDOWS = 'flutter-windows';
|
||||
public const TYPE_FLUTTER_LINUX = 'flutter-linux';
|
||||
public const TYPE_FLUTTER_WEB = 'flutter-web';
|
||||
public const TYPE_APPLE_IOS = 'apple-ios';
|
||||
public const TYPE_APPLE_MACOS = 'apple-macos';
|
||||
public const TYPE_APPLE_WATCHOS = 'apple-watchos';
|
||||
public const TYPE_APPLE_TVOS = 'apple-tvos';
|
||||
public const TYPE_ANDROID = 'android';
|
||||
public const TYPE_WINDOWS = 'windows';
|
||||
public const TYPE_LINUX = 'linux';
|
||||
public const TYPE_UNITY = 'unity';
|
||||
public const TYPE_REACT_NATIVE_IOS = 'react-native-ios';
|
||||
public const TYPE_REACT_NATIVE_ANDROID = 'react-native-android';
|
||||
public const TYPE_SCHEME = 'scheme';
|
||||
|
||||
public const SCHEME_HTTP = 'http';
|
||||
@@ -68,14 +78,24 @@ class Platform
|
||||
|
||||
switch ($type) {
|
||||
case self::TYPE_WEB:
|
||||
case self::TYPE_FLUTTER_WEB:
|
||||
if (!empty($hostname)) {
|
||||
$hostnames[] = $hostname;
|
||||
}
|
||||
break;
|
||||
case self::TYPE_FLUTTER_IOS:
|
||||
case self::TYPE_FLUTTER_ANDROID:
|
||||
case self::TYPE_FLUTTER_MACOS:
|
||||
case self::TYPE_FLUTTER_WINDOWS:
|
||||
case self::TYPE_FLUTTER_LINUX:
|
||||
case self::TYPE_ANDROID:
|
||||
case self::TYPE_WINDOWS:
|
||||
case self::TYPE_LINUX:
|
||||
case self::TYPE_APPLE:
|
||||
case self::TYPE_APPLE_IOS:
|
||||
case self::TYPE_APPLE_MACOS:
|
||||
case self::TYPE_APPLE_WATCHOS:
|
||||
case self::TYPE_APPLE_TVOS:
|
||||
case self::TYPE_REACT_NATIVE_IOS:
|
||||
case self::TYPE_REACT_NATIVE_ANDROID:
|
||||
case self::TYPE_UNITY:
|
||||
if (!empty($key)) {
|
||||
$hostnames[] = $key;
|
||||
}
|
||||
@@ -101,24 +121,37 @@ class Platform
|
||||
}
|
||||
break;
|
||||
case self::TYPE_WEB:
|
||||
case self::TYPE_FLUTTER_WEB:
|
||||
$schemes[] = self::SCHEME_HTTP;
|
||||
$schemes[] = self::SCHEME_HTTPS;
|
||||
break;
|
||||
case self::TYPE_ANDROID:
|
||||
$schemes[] = self::SCHEME_ANDROID;
|
||||
break;
|
||||
case self::TYPE_APPLE:
|
||||
$schemes[] = self::SCHEME_WATCHOS;
|
||||
$schemes[] = self::SCHEME_MACOS;
|
||||
$schemes[] = self::SCHEME_TVOS;
|
||||
case self::TYPE_FLUTTER_IOS:
|
||||
case self::TYPE_APPLE_IOS:
|
||||
case self::TYPE_REACT_NATIVE_IOS:
|
||||
$schemes[] = self::SCHEME_IOS;
|
||||
break;
|
||||
case self::TYPE_WINDOWS:
|
||||
case self::TYPE_FLUTTER_ANDROID:
|
||||
case self::TYPE_ANDROID:
|
||||
case self::TYPE_REACT_NATIVE_ANDROID:
|
||||
$schemes[] = self::SCHEME_ANDROID;
|
||||
break;
|
||||
case self::TYPE_FLUTTER_MACOS:
|
||||
case self::TYPE_APPLE_MACOS:
|
||||
$schemes[] = self::SCHEME_MACOS;
|
||||
break;
|
||||
case self::TYPE_FLUTTER_WINDOWS:
|
||||
case self::TYPE_UNITY:
|
||||
$schemes[] = self::SCHEME_WINDOWS;
|
||||
break;
|
||||
case self::TYPE_LINUX:
|
||||
case self::TYPE_FLUTTER_LINUX:
|
||||
$schemes[] = self::SCHEME_LINUX;
|
||||
break;
|
||||
case self::TYPE_APPLE_WATCHOS:
|
||||
$schemes[] = self::SCHEME_WATCHOS;
|
||||
break;
|
||||
case self::TYPE_APPLE_TVOS:
|
||||
$schemes[] = self::SCHEME_TVOS;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -145,20 +145,9 @@ class Server
|
||||
$paths = $this->paths;
|
||||
$state = $this->state;
|
||||
|
||||
$adapter = new class ($host, $port, ['worker_num' => 1]) extends SwooleAdapter {
|
||||
public function getNativeServer(): SwooleServer
|
||||
{
|
||||
return $this->server;
|
||||
}
|
||||
};
|
||||
|
||||
$nativeServer = $adapter->getNativeServer();
|
||||
|
||||
$container = $adapter->getContainer();
|
||||
$container->set('installerState', fn () => $state);
|
||||
$container->set('installerConfig', fn () => $config);
|
||||
$container->set('installerPaths', fn () => $paths);
|
||||
$container->set('swooleServer', fn () => $nativeServer);
|
||||
Http::setResource('installerState', fn () => $state);
|
||||
Http::setResource('installerConfig', fn () => $config);
|
||||
Http::setResource('installerPaths', fn () => $paths);
|
||||
|
||||
// Register routes via Utopia Platform
|
||||
$platform = new Installer();
|
||||
@@ -171,6 +160,17 @@ class Server
|
||||
->inject('response')
|
||||
->action($errorHandler->action(...));
|
||||
|
||||
$adapter = new class ($host, $port, ['worker_num' => 1]) extends SwooleAdapter {
|
||||
public function getNativeServer(): SwooleServer
|
||||
{
|
||||
return $this->server;
|
||||
}
|
||||
};
|
||||
|
||||
$nativeServer = $adapter->getNativeServer();
|
||||
|
||||
Http::setResource('swooleServer', fn () => $nativeServer);
|
||||
|
||||
$nativeServer->on('start', function () use ($nativeServer, $port, $readyFile) {
|
||||
\Swoole\Process::signal(SIGTERM, fn () => $nativeServer->shutdown());
|
||||
\Swoole\Process::signal(SIGINT, fn () => $nativeServer->shutdown());
|
||||
@@ -180,7 +180,7 @@ class Server
|
||||
}
|
||||
});
|
||||
|
||||
$adapter->onRequest(function (Request $request, Response $response) use ($adapter, $files) {
|
||||
$adapter->onRequest(function (Request $request, Response $response) use ($files) {
|
||||
// Serve static files from memory
|
||||
$uri = $request->getURI();
|
||||
if ($files->isFileLoaded($uri)) {
|
||||
@@ -190,7 +190,7 @@ class Server
|
||||
return;
|
||||
}
|
||||
|
||||
$app = new Http($adapter, 'UTC');
|
||||
$app = new Http('UTC');
|
||||
$app->run($request, $response);
|
||||
});
|
||||
|
||||
|
||||
+2
-2
@@ -207,8 +207,6 @@ class Decrement extends Action
|
||||
->addMetric($this->getDatabasesOperationWriteMetric(), 1)
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), 1);
|
||||
|
||||
$response->dynamic($document, $this->getResponseModel());
|
||||
|
||||
$queueForEvents
|
||||
->setParam('databaseId', $databaseId)
|
||||
->setParam('collectionId', $collectionId)
|
||||
@@ -218,5 +216,7 @@ class Decrement extends Action
|
||||
->setContext('database', $database)
|
||||
->setContext($this->getCollectionsEventsContext(), $collection)
|
||||
->setPayload($response->getPayload(), sensitive: $relationships);
|
||||
|
||||
$response->dynamic($document, $this->getResponseModel());
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -207,8 +207,6 @@ class Increment extends Action
|
||||
->addMetric($this->getDatabasesOperationWriteMetric(), 1)
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), 1);
|
||||
|
||||
$response->dynamic($document, $this->getResponseModel());
|
||||
|
||||
$queueForEvents
|
||||
->setParam('databaseId', $databaseId)
|
||||
->setParam('collectionId', $collectionId)
|
||||
@@ -218,5 +216,7 @@ class Increment extends Action
|
||||
->setContext('database', $database)
|
||||
->setContext($this->getCollectionsEventsContext(), $collection)
|
||||
->setPayload($response->getPayload(), sensitive: $relationships);
|
||||
|
||||
$response->dynamic($document, $this->getResponseModel());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,16 +61,16 @@ class Create extends Action
|
||||
$databaseKeys = System::getEnv('_APP_DATABASE_DOCUMENTSDB_KEYS', '');
|
||||
$databaseOverride = System::getEnv('_APP_DATABASE_DOCUMENTSDB_OVERRIDE');
|
||||
$dbScheme = System::getEnv('_APP_DB_HOST_DOCUMENTSDB', 'mongodb');
|
||||
$databaseSharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', '')));
|
||||
$databaseSharedTablesV1 = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', '')));
|
||||
$databaseSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''));
|
||||
$databaseSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', ''));
|
||||
break;
|
||||
case VECTORSDB:
|
||||
$databases = Config::getParam('pools-vectorsdb', []);
|
||||
$databaseKeys = System::getEnv('_APP_DATABASE_VECTORSDB_KEYS', '');
|
||||
$databaseOverride = System::getEnv('_APP_DATABASE_VECTORSDB_OVERRIDE');
|
||||
$dbScheme = System::getEnv('_APP_DB_HOST_VECTORSDB', 'postgresql');
|
||||
$databaseSharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', '')));
|
||||
$databaseSharedTablesV1 = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', '')));
|
||||
$databaseSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''));
|
||||
$databaseSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', ''));
|
||||
break;
|
||||
default:
|
||||
// legacy/tablesdb
|
||||
@@ -108,7 +108,7 @@ class Create extends Action
|
||||
if ($index !== false) {
|
||||
$selectedDsn = $databases[$index];
|
||||
} else {
|
||||
if (!empty($dsn) && !empty($databaseSharedTables)) {
|
||||
if (!empty($dsn)) {
|
||||
$beforeFilter = \array_values($databases);
|
||||
if ($isSharedTablesV1) {
|
||||
$databases = array_filter($databases, fn ($value) => \in_array($value, $databaseSharedTablesV1));
|
||||
@@ -118,10 +118,7 @@ class Create extends Action
|
||||
$databases = array_filter($databases, fn ($value) => !\in_array($value, $databaseSharedTables));
|
||||
}
|
||||
}
|
||||
if (empty($databases)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, "No {$databasetype} database pool available for the current shared-tables mode");
|
||||
}
|
||||
$selectedDsn = $databases[array_rand($databases)];
|
||||
$selectedDsn = !empty($databases) ? $databases[array_rand($databases)] : '';
|
||||
}
|
||||
|
||||
if (\in_array($selectedDsn, $databaseSharedTables)) {
|
||||
|
||||
@@ -182,33 +182,19 @@ class Update extends Action
|
||||
$dbForDatabases = $getDatabasesDB($databaseDoc);
|
||||
|
||||
try {
|
||||
$transaction = $authorization->skip(fn () => $dbForProject->updateDocument(
|
||||
'transactions',
|
||||
$transactionId,
|
||||
new Document(['status' => 'committing'])
|
||||
));
|
||||
$dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $authorization) {
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
|
||||
'status' => 'committing',
|
||||
])));
|
||||
|
||||
$operations = $authorization->skip(fn () => $dbForProject->find('transactionLogs', [
|
||||
Query::equal('transactionInternalId', [$transaction->getSequence()]),
|
||||
Query::orderAsc(),
|
||||
Query::limit(PHP_INT_MAX),
|
||||
]));
|
||||
$operations = $authorization->skip(fn () => $dbForProject->find('transactionLogs', [
|
||||
Query::equal('transactionInternalId', [$transaction->getSequence()]),
|
||||
Query::orderAsc(),
|
||||
Query::limit(PHP_INT_MAX),
|
||||
]));
|
||||
|
||||
$collections = [];
|
||||
foreach ($operations as $operation) {
|
||||
$databaseInternalId = $operation['databaseInternalId'];
|
||||
$collectionInternalId = $operation['collectionInternalId'];
|
||||
$collectionId = "database_{$databaseInternalId}_collection_{$collectionInternalId}";
|
||||
|
||||
if (!isset($collections[$collectionId])) {
|
||||
$collections[$collectionId] = $authorization->skip(
|
||||
fn () => $dbForProject->getCollection($collectionId)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$dbForDatabases->withTransaction(function () use ($dbForDatabases, $transactionState, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $collections) {
|
||||
$state = [];
|
||||
$collections = [];
|
||||
|
||||
foreach ($operations as $operation) {
|
||||
$databaseInternalId = $operation['databaseInternalId'];
|
||||
@@ -224,6 +210,11 @@ class Update extends Action
|
||||
$data = $data->getArrayCopy();
|
||||
}
|
||||
|
||||
if (!isset($collections[$collectionId])) {
|
||||
$collections[$collectionId] = $authorization->skip(
|
||||
fn () => $dbForProject->getCollection($collectionId)
|
||||
);
|
||||
}
|
||||
$collection = $collections[$collectionId];
|
||||
|
||||
if (\is_array($data) && !empty($data)) {
|
||||
@@ -285,17 +276,16 @@ class Update extends Action
|
||||
}
|
||||
}
|
||||
|
||||
$transaction = $authorization->skip(fn () => $dbForProject->updateDocument(
|
||||
'transactions',
|
||||
$transactionId,
|
||||
new Document(['status' => 'committed'])
|
||||
));
|
||||
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_DOCUMENT)
|
||||
->setDocument($transaction);
|
||||
});
|
||||
|
||||
$transaction = $authorization->skip(fn () => $dbForProject->updateDocument(
|
||||
'transactions',
|
||||
$transactionId,
|
||||
new Document(['status' => 'committed'])
|
||||
));
|
||||
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_DOCUMENT)
|
||||
->setDocument($transaction);
|
||||
} catch (NotFoundException $e) {
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
|
||||
'status' => 'failed',
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Keys;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Datetime;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Create extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'createProjectKey';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/project/keys')
|
||||
->httpAlias('/v1/projects/:projectId/keys')
|
||||
->desc('Create project key')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'keys.write')
|
||||
->label('event', 'keys.[keyId].create')
|
||||
->label('audits.event', 'project.key.create')
|
||||
->label('audits.resource', 'project.key/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'keys',
|
||||
name: 'createKey',
|
||||
description: <<<EOT
|
||||
Create a new API key. It's recommended to have multiple API keys with strict scopes for separate functions within your project.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
model: Response::MODEL_KEY,
|
||||
)
|
||||
],
|
||||
))
|
||||
->param('keyId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
|
||||
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
|
||||
->param('expire', null, new Nullable(new Datetime()), '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('queueForEvents')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string>|null $scopes
|
||||
*/
|
||||
public function action(
|
||||
string $keyId,
|
||||
string $name,
|
||||
?array $scopes,
|
||||
?string $expire,
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
$keyId = ($keyId == 'unique()') ? ID::unique() : $keyId;
|
||||
|
||||
$key = new Document([
|
||||
'$id' => $keyId,
|
||||
'$permissions' => [],
|
||||
'resourceInternalId' => $project->getSequence(),
|
||||
'resourceId' => $project->getId(),
|
||||
'resourceType' => 'projects',
|
||||
'name' => $name,
|
||||
'scopes' => $scopes ?? [],
|
||||
'expire' => $expire,
|
||||
'sdks' => [],
|
||||
'accessedAt' => null,
|
||||
'secret' => API_KEY_STANDARD . '_' . \bin2hex(\random_bytes(128)),
|
||||
]);
|
||||
|
||||
try {
|
||||
$key = $authorization->skip(fn () => $dbForPlatform->createDocument('keys', $key));
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception(Exception::KEY_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
|
||||
$queueForEvents->setParam('keyId', $key->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($key, Response::MODEL_KEY);
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Keys;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class Delete extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'deleteProjectKey';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
|
||||
->setHttpPath('/v1/project/keys/:keyId')
|
||||
->httpAlias('/v1/projects/:projectId/keys/:keyId')
|
||||
->desc('Delete project key')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'keys.write')
|
||||
->label('event', 'keys.[keyId].delete')
|
||||
->label('audits.event', 'project.key.delete')
|
||||
->label('audits.resource', 'project.key/{request.keyId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'keys',
|
||||
name: 'deleteKey',
|
||||
description: <<<EOT
|
||||
Delete a key by its unique ID. Once deleted, the key can no longer be used to authenticate API calls.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_NOCONTENT,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::NONE
|
||||
))
|
||||
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('queueForEvents')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $keyId,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Event $queueForEvents,
|
||||
Document $project,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
$key = $authorization->skip(fn () => $dbForPlatform->getDocument('keys', $keyId));
|
||||
|
||||
if ($key->isEmpty() || $key->getAttribute('resourceType', '') !== 'projects' || $key->getAttribute('resourceInternalId', '') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::KEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!$authorization->skip(fn () => $dbForPlatform->deleteDocument('keys', $key->getId()))) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove document from DB');
|
||||
};
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
|
||||
$queueForEvents->setParam('keyId', $key->getId());
|
||||
|
||||
$response->noContent();
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Keys;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class Get extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'getProjectKey';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/project/keys/:keyId')
|
||||
->httpAlias('/v1/projects/:projectId/keys/:keyId')
|
||||
->desc('Get project key')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'keys.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'keys',
|
||||
name: 'getKey',
|
||||
description: <<<EOT
|
||||
Get a key by its unique ID.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_KEY,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $keyId,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
$key = $authorization->skip(fn () => $dbForPlatform->getDocument('keys', $keyId));
|
||||
|
||||
if ($key->isEmpty() || $key->getAttribute('resourceType', '') !== 'projects' || $key->getAttribute('resourceInternalId', '') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::KEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($key, Response::MODEL_KEY);
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Keys;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Datetime;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Update extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectKey';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
|
||||
->setHttpPath('/v1/project/keys/:keyId')
|
||||
->httpAlias('/v1/projects/:projectId/keys/:keyId')
|
||||
->desc('Update project key')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'keys.write')
|
||||
->label('event', 'keys.[keyId].update')
|
||||
->label('audits.event', 'project.key.update')
|
||||
->label('audits.resource', 'project.key/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'keys',
|
||||
name: 'updateKey',
|
||||
description: <<<EOT
|
||||
Update a key by its unique ID. Use this endpoint to update the name, scopes, or expiration time of an API key.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_KEY,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
|
||||
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
|
||||
->param('expire', null, new Nullable(new Datetime()), '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('queueForEvents')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string>|null $scopes
|
||||
*/
|
||||
public function action(
|
||||
string $keyId,
|
||||
string $name,
|
||||
?array $scopes,
|
||||
?string $expire,
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
$key = $authorization->skip(fn () => $dbForPlatform->getDocument('keys', $keyId));
|
||||
|
||||
if ($key->isEmpty() || $key->getAttribute('resourceType', '') !== 'projects' || $key->getAttribute('resourceInternalId', '') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::KEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$updates = new Document([
|
||||
'name' => $name,
|
||||
'scopes' => $scopes ?? [],
|
||||
'expire' => $expire,
|
||||
]);
|
||||
|
||||
try {
|
||||
$key = $authorization->skip(fn () => $dbForPlatform->updateDocument('keys', $key->getId(), $updates));
|
||||
} catch (Duplicate) {
|
||||
throw new Exception(Exception::KEY_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
|
||||
$queueForEvents->setParam('keyId', $key->getId());
|
||||
|
||||
$response->dynamic($key, Response::MODEL_KEY);
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Keys;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Keys;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Order as OrderException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
|
||||
class XList extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'listProjectKeys';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/project/keys')
|
||||
->httpAlias('/v1/projects/:projectId/keys')
|
||||
->desc('List project keys')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'keys.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'keys',
|
||||
name: 'listKeys',
|
||||
description: <<<EOT
|
||||
Get a list of all API keys from the current project.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_KEY_LIST,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('queries', [], new Keys(), '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(', ', Keys::ALLOWED_ATTRIBUTES), true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('project')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $queries
|
||||
*/
|
||||
public function action(
|
||||
array $queries,
|
||||
bool $includeTotal,
|
||||
Document $project,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
if (\count(Query::getByType($queries, [Query::TYPE_LIMIT])) === 0) {
|
||||
$queries[] = Query::limit(5000);
|
||||
}
|
||||
|
||||
$queries[] = Query::equal('resourceType', ['projects']);
|
||||
$queries[] = Query::equal('resourceInternalId', [$project->getSequence()]);
|
||||
|
||||
$cursor = Query::getCursorQueries($queries, false);
|
||||
$cursor = \reset($cursor);
|
||||
|
||||
if ($cursor !== false) {
|
||||
$validator = new Cursor();
|
||||
if (!$validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$keyId = $cursor->getValue();
|
||||
$cursorDocument = $authorization->skip(fn () => $dbForPlatform->findOne('keys', [
|
||||
Query::equal('$id', [$keyId]),
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$project->getSequence()]),
|
||||
]));
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Key '{$keyId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
try {
|
||||
$keys = $authorization->skip(fn () => $dbForPlatform->find('keys', $queries));
|
||||
$total = $includeTotal ? $authorization->skip(fn () => $dbForPlatform->count('keys', $filterQueries, APP_LIMIT_COUNT)) : 0;
|
||||
} catch (OrderException $e) {
|
||||
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'keys' => $keys,
|
||||
'total' => $total,
|
||||
]), Response::MODEL_KEY_LIST);
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Create extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'createProjectAndroidPlatform';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/project/platforms/android')
|
||||
->desc('Create project Android platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].create')
|
||||
->label('audits.event', 'project.platform.create')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'platforms',
|
||||
name: 'createAndroidPlatform',
|
||||
description: <<<EOT
|
||||
Create a new Android platform for your project. Use this endpoint to register a new Android platform where your users will run your application which will interact with the Appwrite API.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
model: Response::MODEL_PLATFORM_ANDROID,
|
||||
)
|
||||
],
|
||||
))
|
||||
->param('platformId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
|
||||
->param('applicationId', '', new Text(256), 'Android application ID. Max length: 256 chars.')
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
->inject('project')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $platformId,
|
||||
string $name,
|
||||
string $applicationId,
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
Document $project,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
$platformId = ($platformId == 'unique()') ? ID::unique() : $platformId;
|
||||
|
||||
$platform = new Document([
|
||||
'$id' => $platformId,
|
||||
'$permissions' => [],
|
||||
'projectInternalId' => $project->getSequence(),
|
||||
'projectId' => $project->getId(),
|
||||
'type' => Platform::TYPE_ANDROID,
|
||||
'name' => $name,
|
||||
'key' => $applicationId,
|
||||
'hostname' => '',
|
||||
]);
|
||||
|
||||
try {
|
||||
$platform = $authorization->skip(fn () => $dbForPlatform->createDocument('platforms', $platform));
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
|
||||
$queueForEvents->setParam('platformId', $platform->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($platform, Response::MODEL_PLATFORM_ANDROID);
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectAndroidPlatform';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
|
||||
->setHttpPath('/v1/project/platforms/android/:platformId')
|
||||
->desc('Update project Android platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].update')
|
||||
->label('audits.event', 'project.platform.update')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'platforms',
|
||||
name: 'updateAndroidPlatform',
|
||||
description: <<<EOT
|
||||
Update an Android platform by its unique ID. Use this endpoint to update the platform's name or application ID.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PLATFORM_ANDROID,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
|
||||
->param('applicationId', '', new Text(256), 'Android application ID. Max length: 256 chars.')
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('project')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $platformId,
|
||||
string $name,
|
||||
string $applicationId,
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
Document $project,
|
||||
) {
|
||||
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
|
||||
|
||||
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::PLATFORM_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($platform->getAttribute('type', '') !== Platform::TYPE_ANDROID) {
|
||||
throw new Exception(Exception::PLATFORM_METHOD_UNSUPPORTED);
|
||||
}
|
||||
|
||||
$updates = new Document([
|
||||
'name' => $name,
|
||||
'key' => $applicationId,
|
||||
]);
|
||||
|
||||
try {
|
||||
$platform = $authorization->skip(fn () => $dbForPlatform->updateDocument('platforms', $platform->getId(), $updates));
|
||||
} catch (Duplicate) {
|
||||
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
|
||||
$queueForEvents->setParam('platformId', $platform->getId());
|
||||
|
||||
$response->dynamic($platform, Response::MODEL_PLATFORM_ANDROID);
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Apple;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Create extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'createProjectApplePlatform';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/project/platforms/apple')
|
||||
->desc('Create project Apple platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].create')
|
||||
->label('audits.event', 'project.platform.create')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'platforms',
|
||||
name: 'createApplePlatform',
|
||||
description: <<<EOT
|
||||
Create a new Apple platform for your project. Use this endpoint to register a new Apple platform where your users will run your application which will interact with the Appwrite API.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
model: Response::MODEL_PLATFORM_APPLE,
|
||||
)
|
||||
],
|
||||
))
|
||||
->param('platformId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
|
||||
->param('bundleIdentifier', '', new Text(256), 'Apple bundle identifier. Max length: 256 chars.')
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
->inject('project')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $platformId,
|
||||
string $name,
|
||||
string $bundleIdentifier,
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
Document $project,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
$platformId = ($platformId == 'unique()') ? ID::unique() : $platformId;
|
||||
|
||||
$platform = new Document([
|
||||
'$id' => $platformId,
|
||||
'$permissions' => [],
|
||||
'projectInternalId' => $project->getSequence(),
|
||||
'projectId' => $project->getId(),
|
||||
'type' => Platform::TYPE_APPLE,
|
||||
'name' => $name,
|
||||
'key' => $bundleIdentifier,
|
||||
'hostname' => '',
|
||||
]);
|
||||
|
||||
try {
|
||||
$platform = $authorization->skip(fn () => $dbForPlatform->createDocument('platforms', $platform));
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
|
||||
$queueForEvents->setParam('platformId', $platform->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($platform, Response::MODEL_PLATFORM_APPLE);
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Apple;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectApplePlatform';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
|
||||
->setHttpPath('/v1/project/platforms/apple/:platformId')
|
||||
->desc('Update project Apple platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].update')
|
||||
->label('audits.event', 'project.platform.update')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'platforms',
|
||||
name: 'updateApplePlatform',
|
||||
description: <<<EOT
|
||||
Update an Apple platform by its unique ID. Use this endpoint to update the platform's name or bundle identifier.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PLATFORM_APPLE,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
|
||||
->param('bundleIdentifier', '', new Text(256), 'Apple bundle identifier. Max length: 256 chars.')
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('project')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $platformId,
|
||||
string $name,
|
||||
string $bundleIdentifier,
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
Document $project,
|
||||
) {
|
||||
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
|
||||
|
||||
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::PLATFORM_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($platform->getAttribute('type', '') !== Platform::TYPE_APPLE) {
|
||||
throw new Exception(Exception::PLATFORM_METHOD_UNSUPPORTED);
|
||||
}
|
||||
|
||||
$updates = new Document([
|
||||
'name' => $name,
|
||||
'key' => $bundleIdentifier,
|
||||
]);
|
||||
|
||||
try {
|
||||
$platform = $authorization->skip(fn () => $dbForPlatform->updateDocument('platforms', $platform->getId(), $updates));
|
||||
} catch (Duplicate) {
|
||||
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
|
||||
$queueForEvents->setParam('platformId', $platform->getId());
|
||||
|
||||
$response->dynamic($platform, Response::MODEL_PLATFORM_APPLE);
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class Delete extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'deleteProjectPlatform';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
|
||||
->setHttpPath('/v1/project/platforms/:platformId')
|
||||
->httpAlias('/v1/projects/:projectId/platforms/:platformId')
|
||||
->desc('Delete project platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].delete')
|
||||
->label('audits.event', 'project.platform.delete')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'platforms',
|
||||
name: 'deletePlatform',
|
||||
description: <<<EOT
|
||||
Delete a platform by its unique ID. This endpoint removes the platform and all its configurations from the project.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_NOCONTENT,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::NONE
|
||||
))
|
||||
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('project')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $platformId,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
Document $project,
|
||||
Event $queueForEvents,
|
||||
) {
|
||||
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
|
||||
|
||||
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::PLATFORM_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!$authorization->skip(fn () => $dbForPlatform->deleteDocument('platforms', $platform->getId()))) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove document from DB');
|
||||
};
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
|
||||
$queueForEvents->setParam('platformId', $platform->getId());
|
||||
|
||||
$response->noContent();
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'getProjectPlatform';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/project/platforms/:platformId')
|
||||
->httpAlias('/v1/projects/:projectId/platforms/:platformId')
|
||||
->desc('Get project platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'platforms.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'platforms',
|
||||
name: 'getPlatform',
|
||||
description: <<<EOT
|
||||
Get a platform by its unique ID. This endpoint returns the platform's details, including its name, type, and key configurations.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: [
|
||||
Response::MODEL_PLATFORM_WEB,
|
||||
Response::MODEL_PLATFORM_APPLE,
|
||||
Response::MODEL_PLATFORM_ANDROID,
|
||||
Response::MODEL_PLATFORM_WINDOWS,
|
||||
Response::MODEL_PLATFORM_LINUX,
|
||||
],
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('project')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $platformId,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
Document $project
|
||||
) {
|
||||
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
|
||||
|
||||
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::PLATFORM_NOT_FOUND);
|
||||
}
|
||||
|
||||
$type = $platform->getAttribute('type');
|
||||
|
||||
$model = match($type) {
|
||||
Platform::TYPE_WEB => Response::MODEL_PLATFORM_WEB,
|
||||
Platform::TYPE_APPLE => Response::MODEL_PLATFORM_APPLE,
|
||||
Platform::TYPE_ANDROID => Response::MODEL_PLATFORM_ANDROID,
|
||||
Platform::TYPE_WINDOWS => Response::MODEL_PLATFORM_WINDOWS,
|
||||
Platform::TYPE_LINUX => Response::MODEL_PLATFORM_LINUX,
|
||||
default => Response::MODEL_PLATFORM_WEB // Backwards compatibility
|
||||
};
|
||||
|
||||
$response->dynamic($platform, $model);
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Linux;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Create extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'createProjectLinuxPlatform';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/project/platforms/linux')
|
||||
->desc('Create project Linux platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].create')
|
||||
->label('audits.event', 'project.platform.create')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'platforms',
|
||||
name: 'createLinuxPlatform',
|
||||
description: <<<EOT
|
||||
Create a new Linux platform for your project. Use this endpoint to register a new Linux platform where your users will run your application which will interact with the Appwrite API.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
model: Response::MODEL_PLATFORM_LINUX,
|
||||
)
|
||||
],
|
||||
))
|
||||
->param('platformId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
|
||||
->param('packageName', '', new Text(256), 'Linux package name. Max length: 256 chars.')
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
->inject('project')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $platformId,
|
||||
string $name,
|
||||
string $packageName,
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
Document $project,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
$platformId = ($platformId == 'unique()') ? ID::unique() : $platformId;
|
||||
|
||||
$platform = new Document([
|
||||
'$id' => $platformId,
|
||||
'$permissions' => [],
|
||||
'projectInternalId' => $project->getSequence(),
|
||||
'projectId' => $project->getId(),
|
||||
'type' => Platform::TYPE_LINUX,
|
||||
'name' => $name,
|
||||
'key' => $packageName,
|
||||
'hostname' => '', // Web platform attribute
|
||||
]);
|
||||
|
||||
try {
|
||||
$platform = $authorization->skip(fn () => $dbForPlatform->createDocument('platforms', $platform));
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
|
||||
$queueForEvents->setParam('platformId', $platform->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($platform, Response::MODEL_PLATFORM_LINUX);
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Linux;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectLinuxPlatform';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
|
||||
->setHttpPath('/v1/project/platforms/linux/:platformId')
|
||||
->desc('Update project Linux platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].update')
|
||||
->label('audits.event', 'project.platform.update')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'platforms',
|
||||
name: 'updateLinuxPlatform',
|
||||
description: <<<EOT
|
||||
Update a Linux platform by its unique ID. Use this endpoint to update the platform's name or package name.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PLATFORM_LINUX,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
|
||||
->param('packageName', '', new Text(256), 'Linux package name. Max length: 256 chars.')
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('project')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $platformId,
|
||||
string $name,
|
||||
string $packageName,
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
Document $project,
|
||||
) {
|
||||
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
|
||||
|
||||
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::PLATFORM_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($platform->getAttribute('type', '') !== Platform::TYPE_LINUX) {
|
||||
throw new Exception(Exception::PLATFORM_METHOD_UNSUPPORTED);
|
||||
}
|
||||
|
||||
$updates = new Document([
|
||||
'name' => $name,
|
||||
'key' => $packageName,
|
||||
]);
|
||||
|
||||
try {
|
||||
$platform = $authorization->skip(fn () => $dbForPlatform->updateDocument('platforms', $platform->getId(), $updates));
|
||||
} catch (Duplicate) {
|
||||
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
|
||||
$queueForEvents->setParam('platformId', $platform->getId());
|
||||
|
||||
$response->dynamic($platform, Response::MODEL_PLATFORM_LINUX);
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Web;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Hostname;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
/**
|
||||
* WARNING: This kind of platform has most complex action, because it holds backwards compatibility too.
|
||||
* If possible, refer to any other type of platform for APIs, for more simpler endpoint.
|
||||
*/
|
||||
class Create extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'createProjectWebPlatform';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/project/platforms/web')
|
||||
->httpAlias('/v1/projects/:projectId/platforms')
|
||||
->desc('Create project web platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].create')
|
||||
->label('audits.event', 'project.platform.create')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'platforms',
|
||||
name: 'createWebPlatform',
|
||||
description: <<<EOT
|
||||
Create a new web platform for your project. Use this endpoint to register a new platform where your users will run your application which will interact with the Appwrite API.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
model: Response::MODEL_PLATFORM_WEB,
|
||||
)
|
||||
],
|
||||
))
|
||||
->param('platformId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
|
||||
->param('hostname', '', new Hostname(), 'Platform web hostname. Max length: 256 chars.', optional: true) // Optional for backwards compatibility
|
||||
->param('key', '', new Text(256), 'Deprecated: Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', optional: true, deprecated: true) // Exists for backwards compatibility
|
||||
->param('type', '', new Text(256), 'Deprecated: Platform type. Max length: 256 chars.', optional: true, deprecated: true) // Exists for backwards compatibility
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
->inject('project')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $platformId,
|
||||
string $name,
|
||||
string $hostname,
|
||||
?string $key, // For backwards compatibility
|
||||
?string $type, // For backwards compatibility
|
||||
Request $request,
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
Document $project,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
$key = $key ?? ''; // App platform attribute, backwards compatibility
|
||||
$type = $type ?? ''; // App platform attribute, backwards compatibility
|
||||
|
||||
// Backwards compatibility
|
||||
// Used to have: type, name, key, hostname
|
||||
if (!empty($type)) {
|
||||
// Validate deprecated type, and rename to new type
|
||||
$deprecatedTypeMapping = [
|
||||
// Web
|
||||
'web' => Platform::TYPE_WEB,
|
||||
'flutter-web' => Platform::TYPE_WEB,
|
||||
'unity' => Platform::TYPE_WEB, // Was not officially supported anyway
|
||||
|
||||
// Apple
|
||||
'flutter-macos' => Platform::TYPE_APPLE,
|
||||
'flutter-ios' => Platform::TYPE_APPLE,
|
||||
'react-native-ios' => Platform::TYPE_APPLE,
|
||||
'apple-ios' => Platform::TYPE_APPLE,
|
||||
'apple-macos' => Platform::TYPE_APPLE,
|
||||
'apple-watchos' => Platform::TYPE_APPLE,
|
||||
'apple-tvos' => Platform::TYPE_APPLE,
|
||||
|
||||
// Android
|
||||
'flutter-android' => Platform::TYPE_ANDROID,
|
||||
'android' => Platform::TYPE_ANDROID,
|
||||
'react-native-android' => Platform::TYPE_ANDROID,
|
||||
|
||||
'flutter-linux' => Platform::TYPE_LINUX,
|
||||
'flutter-windows' => Platform::TYPE_WINDOWS,
|
||||
];
|
||||
|
||||
$typeValidator = new WhiteList(\array_keys($deprecatedTypeMapping));
|
||||
if (!$typeValidator->isValid($request->getParam('type', ''))) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "type" is invalid: ' . $typeValidator->getDescription());
|
||||
}
|
||||
|
||||
$type = $deprecatedTypeMapping[$request->getParam('type', '')] ?? '';
|
||||
}
|
||||
|
||||
if (!empty($key)) {
|
||||
// Validate deprecated app id (key)
|
||||
$keyValidator = new Text(256);
|
||||
if (!$keyValidator->isValid($key)) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "key" is invalid: ' . $keyValidator->getDescription());
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($key) && empty($type)) {
|
||||
// Modern request, validate hostname
|
||||
if (empty($hostname)) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "hostname" is not optional.');
|
||||
}
|
||||
}
|
||||
|
||||
$platformId = ($platformId == 'unique()') ? ID::unique() : $platformId;
|
||||
|
||||
$platform = new Document([
|
||||
'$id' => $platformId,
|
||||
'$permissions' => [],
|
||||
'projectInternalId' => $project->getSequence(),
|
||||
'projectId' => $project->getId(),
|
||||
'type' => $type ?: Platform::TYPE_WEB, // Preserve type for backwards compatibility
|
||||
'name' => $name,
|
||||
'key' => $key,
|
||||
'hostname' => $hostname
|
||||
]);
|
||||
|
||||
try {
|
||||
$platform = $authorization->skip(fn () => $dbForPlatform->createDocument('platforms', $platform));
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
|
||||
$queueForEvents->setParam('platformId', $platform->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($platform, Response::MODEL_PLATFORM_WEB);
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Web;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Hostname;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectWebPlatform';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
|
||||
->setHttpPath('/v1/project/platforms/web/:platformId')
|
||||
->httpAlias('/v1/projects/:projectId/platforms/:platformId')
|
||||
->desc('Update project web platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].update')
|
||||
->label('audits.event', 'project.platform.update')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'platforms',
|
||||
name: 'updateWebPlatform',
|
||||
description: <<<EOT
|
||||
Update a web platform by its unique ID. Use this endpoint to update the platform's name or hostname.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PLATFORM_WEB,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
|
||||
->param('hostname', '', new Hostname(), 'Platform web hostname. Max length: 256 chars.', optional: true) // Optional for backwards compatibility
|
||||
->param('key', '', new Text(256), 'Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', optional: true, deprecated: true) // Exists for backwards compatibility
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('project')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $platformId,
|
||||
string $name,
|
||||
string $hostname,
|
||||
?string $key, // For backwards compatibility
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
Document $project,
|
||||
) {
|
||||
$key = $key ?? ''; // App platform attribute, backwards compatibility
|
||||
|
||||
// Backwards compatibility
|
||||
// Used to have: type, name, key, hostname
|
||||
if (!empty($key)) {
|
||||
// Validate deprecated app id (key)
|
||||
$keyValidator = new Text(256);
|
||||
if (!$keyValidator->isValid($key)) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "key" is invalid: ' . $keyValidator->getDescription());
|
||||
}
|
||||
}
|
||||
|
||||
// One day, ideally, we ensure hostname is not empty
|
||||
// But for backwards compatibility backend must threat it as optional for now
|
||||
|
||||
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
|
||||
|
||||
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::PLATFORM_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Wrapped in if, for backwards compatibility
|
||||
if (!empty($hostname)) {
|
||||
$supportedTypes = [
|
||||
Platform::TYPE_WEB,
|
||||
// Backwards compatibility
|
||||
'flutter-web',
|
||||
'unity',
|
||||
'flutter-macos',
|
||||
'flutter-ios',
|
||||
'react-native-ios',
|
||||
'apple-ios',
|
||||
'apple-macos',
|
||||
'apple-watchos',
|
||||
'apple-tvos',
|
||||
'flutter-android',
|
||||
'react-native-android',
|
||||
'flutter-windows',
|
||||
'flutter-linux',
|
||||
];
|
||||
if (!in_array($platform->getAttribute('type', ''), $supportedTypes)) {
|
||||
throw new Exception(Exception::PLATFORM_METHOD_UNSUPPORTED);
|
||||
}
|
||||
}
|
||||
|
||||
$updates = new Document([
|
||||
'name' => $name,
|
||||
]);
|
||||
|
||||
// Wrapped in if, for backwards compatibility
|
||||
if (!empty($hostname)) {
|
||||
$updates->setAttribute('hostname', $hostname);
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
if (!empty($key)) {
|
||||
$updates->setAttribute('key', $key);
|
||||
}
|
||||
|
||||
try {
|
||||
$platform = $authorization->skip(fn () => $dbForPlatform->updateDocument('platforms', $platform->getId(), $updates));
|
||||
} catch (Duplicate) {
|
||||
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
|
||||
$queueForEvents->setParam('platformId', $platform->getId());
|
||||
|
||||
$response->dynamic($platform, Response::MODEL_PLATFORM_WEB);
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Create extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'createProjectWindowsPlatform';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/project/platforms/windows')
|
||||
->desc('Create project Windows platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].create')
|
||||
->label('audits.event', 'project.platform.create')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'platforms',
|
||||
name: 'createWindowsPlatform',
|
||||
description: <<<EOT
|
||||
Create a new Windows platform for your project. Use this endpoint to register a new Windows platform where your users will run your application which will interact with the Appwrite API.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
model: Response::MODEL_PLATFORM_WINDOWS,
|
||||
)
|
||||
],
|
||||
))
|
||||
->param('platformId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
|
||||
->param('packageIdentifierName', '', new Text(256), 'Windows package identifier name. Max length: 256 chars.')
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
->inject('project')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $platformId,
|
||||
string $name,
|
||||
string $packageIdentifierName,
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
Document $project,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
$platformId = ($platformId == 'unique()') ? ID::unique() : $platformId;
|
||||
|
||||
$platform = new Document([
|
||||
'$id' => $platformId,
|
||||
'$permissions' => [],
|
||||
'projectInternalId' => $project->getSequence(),
|
||||
'projectId' => $project->getId(),
|
||||
'type' => Platform::TYPE_WINDOWS,
|
||||
'name' => $name,
|
||||
'key' => $packageIdentifierName,
|
||||
'hostname' => '',
|
||||
]);
|
||||
|
||||
try {
|
||||
$platform = $authorization->skip(fn () => $dbForPlatform->createDocument('platforms', $platform));
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
|
||||
$queueForEvents->setParam('platformId', $platform->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($platform, Response::MODEL_PLATFORM_WINDOWS);
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectWindowsPlatform';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
|
||||
->setHttpPath('/v1/project/platforms/windows/:platformId')
|
||||
->desc('Update project Windows platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].update')
|
||||
->label('audits.event', 'project.platform.update')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'platforms',
|
||||
name: 'updateWindowsPlatform',
|
||||
description: <<<EOT
|
||||
Update a Windows platform by its unique ID. Use this endpoint to update the platform's name or package identifier name.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PLATFORM_WINDOWS,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
|
||||
->param('packageIdentifierName', '', new Text(256), 'Windows package identifier name. Max length: 256 chars.')
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('project')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $platformId,
|
||||
string $name,
|
||||
string $packageIdentifierName,
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
Document $project,
|
||||
) {
|
||||
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
|
||||
|
||||
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::PLATFORM_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($platform->getAttribute('type', '') !== Platform::TYPE_WINDOWS) {
|
||||
throw new Exception(Exception::PLATFORM_METHOD_UNSUPPORTED);
|
||||
}
|
||||
|
||||
$updates = new Document([
|
||||
'name' => $name,
|
||||
'key' => $packageIdentifierName,
|
||||
]);
|
||||
|
||||
try {
|
||||
$platform = $authorization->skip(fn () => $dbForPlatform->updateDocument('platforms', $platform->getId(), $updates));
|
||||
} catch (Duplicate) {
|
||||
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
|
||||
$queueForEvents->setParam('platformId', $platform->getId());
|
||||
|
||||
$response->dynamic($platform, Response::MODEL_PLATFORM_WINDOWS);
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Platforms;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Order as OrderException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
|
||||
class XList extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'listProjectPlatforms';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/project/platforms')
|
||||
->httpAlias('/v1/projects/:projectId/platforms')
|
||||
->desc('List project platforms')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'platforms.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'platforms',
|
||||
name: 'listPlatforms',
|
||||
description: <<<EOT
|
||||
Get a list of all platforms in the project. This endpoint returns an array of all platforms and their configurations.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PLATFORM_LIST,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('queries', [], new Platforms(), '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(', ', Platforms::ALLOWED_ATTRIBUTES), true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('project')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $queries
|
||||
*/
|
||||
public function action(
|
||||
array $queries,
|
||||
bool $includeTotal,
|
||||
Document $project,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
foreach ($queries as $query) {
|
||||
if (\in_array($query->getAttribute(), ['bundleIdentifier', 'applicationId', 'packageIdentifierName', 'packageName'])) {
|
||||
$query->setAttribute('key');
|
||||
}
|
||||
}
|
||||
|
||||
$queries[] = Query::equal('projectInternalId', [$project->getSequence()]);
|
||||
|
||||
$cursor = Query::getCursorQueries($queries, false);
|
||||
$cursor = \reset($cursor);
|
||||
|
||||
if ($cursor !== false) {
|
||||
$validator = new Cursor();
|
||||
if (!$validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$platformId = $cursor->getValue();
|
||||
$cursorDocument = $authorization->skip(fn () => $dbForPlatform->findOne('platforms', [
|
||||
Query::equal('$id', [$platformId]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
]));
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Platform '{$platformId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
try {
|
||||
$platforms = $authorization->skip(fn () => $dbForPlatform->find('platforms', $queries));
|
||||
$total = $includeTotal ? $authorization->skip(fn () => $dbForPlatform->count('platforms', $filterQueries, APP_LIMIT_COUNT)) : 0;
|
||||
} catch (OrderException $e) {
|
||||
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'platforms' => $platforms,
|
||||
'total' => $total,
|
||||
]), Response::MODEL_PLATFORM_LIST);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\Variables;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
@@ -18,7 +19,7 @@ use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Create extends Action
|
||||
class Create extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\Variables;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
@@ -15,7 +16,7 @@ use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class Delete extends Action
|
||||
class Delete extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
@@ -34,7 +35,7 @@ class Delete extends Action
|
||||
->label('scope', 'project.write')
|
||||
->label('event', 'variables.[variableId].delete')
|
||||
->label('audits.event', 'project.variable.delete')
|
||||
->label('audits.resource', 'project.variable/{request.variableId}')
|
||||
->label('audits.resource', 'project.variable/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'variables',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Variables;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
@@ -12,7 +13,7 @@ use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class Get extends Action
|
||||
class Get extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\Variables;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
@@ -18,7 +19,7 @@ use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Update extends Action
|
||||
class Update extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Variables;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
@@ -18,7 +19,7 @@ use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
|
||||
class XList extends Action
|
||||
class XList extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
|
||||
@@ -3,25 +3,6 @@
|
||||
namespace Appwrite\Platform\Modules\Project\Services;
|
||||
|
||||
use Appwrite\Platform\Modules\Project\Http\Init;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Keys\Create as CreateKey;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Keys\Delete as DeleteKey;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Keys\Get as GetKey;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Keys\Update as UpdateKey;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Keys\XList as ListKeys;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Labels\Update as UpdateProjectLabels;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android\Create as CreateAndroidPlatform;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android\Update as UpdateAndroidPlatform;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Apple\Create as CreateApplePlatform;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Apple\Update as UpdateApplePlatform;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Delete as DeletePlatform;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Get as GetPlatform;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Linux\Create as CreateLinuxPlatform;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Linux\Update as UpdateLinuxPlatform;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Web\Create as CreateWebPlatform;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Web\Update as UpdateWebPlatform;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Create as CreateWindowsPlatform;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Update as UpdateWindowsPlatform;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\XList as ListPlatforms;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Create as CreateVariable;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Delete as DeleteVariable;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Get as GetVariable;
|
||||
@@ -39,35 +20,10 @@ class Http extends Service
|
||||
$this->addAction(Init::getName(), new Init());
|
||||
|
||||
// Project
|
||||
$this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels());
|
||||
|
||||
// Variables
|
||||
$this->addAction(CreateVariable::getName(), new CreateVariable());
|
||||
$this->addAction(ListVariables::getName(), new ListVariables());
|
||||
$this->addAction(GetVariable::getName(), new GetVariable());
|
||||
$this->addAction(DeleteVariable::getName(), new DeleteVariable());
|
||||
$this->addAction(UpdateVariable::getName(), new UpdateVariable());
|
||||
|
||||
// Keys
|
||||
$this->addAction(CreateKey::getName(), new CreateKey());
|
||||
$this->addAction(ListKeys::getName(), new ListKeys());
|
||||
$this->addAction(GetKey::getName(), new GetKey());
|
||||
$this->addAction(DeleteKey::getName(), new DeleteKey());
|
||||
$this->addAction(UpdateKey::getName(), new UpdateKey());
|
||||
|
||||
// Platforms
|
||||
$this->addAction(DeletePlatform::getName(), new DeletePlatform());
|
||||
$this->addAction(UpdateWebPlatform::getName(), new UpdateWebPlatform());
|
||||
$this->addAction(UpdateApplePlatform::getName(), new UpdateApplePlatform());
|
||||
$this->addAction(UpdateAndroidPlatform::getName(), new UpdateAndroidPlatform());
|
||||
$this->addAction(UpdateWindowsPlatform::getName(), new UpdateWindowsPlatform());
|
||||
$this->addAction(UpdateLinuxPlatform::getName(), new UpdateLinuxPlatform());
|
||||
$this->addAction(CreateWebPlatform::getName(), new CreateWebPlatform());
|
||||
$this->addAction(CreateApplePlatform::getName(), new CreateApplePlatform());
|
||||
$this->addAction(CreateAndroidPlatform::getName(), new CreateAndroidPlatform());
|
||||
$this->addAction(CreateWindowsPlatform::getName(), new CreateWindowsPlatform());
|
||||
$this->addAction(CreateLinuxPlatform::getName(), new CreateLinuxPlatform());
|
||||
$this->addAction(GetPlatform::getName(), new GetPlatform());
|
||||
$this->addAction(ListPlatforms::getName(), new ListPlatforms());
|
||||
}
|
||||
}
|
||||
|
||||
+29
-16
@@ -1,15 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Labels;
|
||||
namespace Appwrite\Platform\Modules\Projects\Http\Projects\Labels;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Projects;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
@@ -22,37 +27,39 @@ class Update extends Action
|
||||
return 'updateProjectLabels';
|
||||
}
|
||||
|
||||
protected function getQueriesValidator(): Validator
|
||||
{
|
||||
return new Projects();
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
|
||||
->setHttpPath('/v1/project/labels')
|
||||
->httpAlias('/v1/projects/:projectId/labels')
|
||||
->setHttpPath('/v1/projects/:projectId/labels')
|
||||
->desc('Update project labels')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('event', 'labels.*.update')
|
||||
->label('audits.event', 'project.labels.update')
|
||||
->label('audits.resource', 'project.labels/{response.$id}')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: null,
|
||||
namespace: 'projects',
|
||||
group: 'projects',
|
||||
name: 'updateLabels',
|
||||
description: <<<EOT
|
||||
Update the project labels. Labels can be used to easily filter projects in an organization.
|
||||
Update the project labels by its unique ID. Labels can be used to easily filter projects in an organization.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
model: Response::MODEL_PROJECT
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('labels', [], new ArrayList(new Text(36, allowList: [...Text::NUMBERS, ...Text::ALPHABET_UPPER, ...Text::ALPHABET_LOWER]), APP_LIMIT_ARRAY_LABELS_SIZE), 'Array of project labels. Replaces the previous labels. Maximum of ' . APP_LIMIT_ARRAY_LABELS_SIZE . ' labels are allowed, each up to 36 alphanumeric characters long.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -60,11 +67,17 @@ class Update extends Action
|
||||
* @param array<string> $labels
|
||||
*/
|
||||
public function action(
|
||||
string $projectId,
|
||||
array $labels,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project
|
||||
Database $dbForPlatform
|
||||
): void {
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$labels = (array) \array_values(\array_unique($labels));
|
||||
|
||||
$project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document(['labels' => $labels]));
|
||||
@@ -8,6 +8,7 @@ use Appwrite\Platform\Modules\Projects\Http\DevKeys\Get as GetDevKey;
|
||||
use Appwrite\Platform\Modules\Projects\Http\DevKeys\Update as UpdateDevKey;
|
||||
use Appwrite\Platform\Modules\Projects\Http\DevKeys\XList as ListDevKeys;
|
||||
use Appwrite\Platform\Modules\Projects\Http\Projects\Create as CreateProject;
|
||||
use Appwrite\Platform\Modules\Projects\Http\Projects\Labels\Update as UpdateProjectLabels;
|
||||
use Appwrite\Platform\Modules\Projects\Http\Projects\Team\Update as UpdateProjectTeam;
|
||||
use Appwrite\Platform\Modules\Projects\Http\Projects\Update as UpdateProject;
|
||||
use Appwrite\Platform\Modules\Projects\Http\Projects\XList as ListProjects;
|
||||
@@ -30,6 +31,7 @@ class Http extends Service
|
||||
$this->addAction(CreateProject::getName(), new CreateProject());
|
||||
$this->addAction(UpdateProject::getName(), new UpdateProject());
|
||||
$this->addAction(ListProjects::getName(), new ListProjects());
|
||||
$this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels());
|
||||
$this->addAction(UpdateProjectTeam::getName(), new UpdateProjectTeam());
|
||||
|
||||
$this->addAction(CreateSchedule::getName(), new CreateSchedule());
|
||||
|
||||
@@ -74,12 +74,10 @@ class Update extends Action
|
||||
->inject('queueForEvents')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken, bool $domainVerification, ?string $cookieDomain)
|
||||
public function action(string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken)
|
||||
{
|
||||
$protocol = $request->getProtocol();
|
||||
|
||||
@@ -164,7 +162,7 @@ class Update extends Action
|
||||
->setProperty('secret', $secret)
|
||||
->encode();
|
||||
|
||||
if (!$domainVerification) {
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
|
||||
}
|
||||
|
||||
@@ -174,7 +172,7 @@ class Update extends Action
|
||||
value: $encoded,
|
||||
expire: (new \DateTime($expire))->getTimestamp(),
|
||||
path: '/',
|
||||
domain: $cookieDomain,
|
||||
domain: Config::getParam('cookieDomain'),
|
||||
secure: ('https' === $protocol),
|
||||
httponly: true
|
||||
)
|
||||
@@ -183,7 +181,7 @@ class Update extends Action
|
||||
value: $encoded,
|
||||
expire: (new \DateTime($expire))->getTimestamp(),
|
||||
path: '/',
|
||||
domain: $cookieDomain,
|
||||
domain: Config::getParam('cookieDomain'),
|
||||
secure: ('https' === $protocol),
|
||||
httponly: true,
|
||||
sameSite: Config::getParam('cookieSamesite')
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks;
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Event\Validator\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
@@ -24,7 +25,7 @@ use Utopia\Validator\Multiple;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\URL;
|
||||
|
||||
class Create extends Action
|
||||
class Create extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
@@ -17,7 +18,7 @@ use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class Delete extends Action
|
||||
class Delete extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
@@ -15,7 +16,7 @@ use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class Get extends Action
|
||||
class Get extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Signature;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
@@ -16,7 +17,7 @@ use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class Update extends Action
|
||||
class Update extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks;
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Event\Validator\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
@@ -23,7 +24,7 @@ use Utopia\Validator\Multiple;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\URL;
|
||||
|
||||
class Update extends Action
|
||||
class Update extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
@@ -19,7 +20,7 @@ use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
|
||||
class XList extends Action
|
||||
class XList extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Registry\Registry;
|
||||
use Utopia\Validator\Text;
|
||||
@@ -31,7 +32,6 @@ class Migrate extends Action
|
||||
->inject('getProjectDB')
|
||||
->inject('register')
|
||||
->inject('authorization')
|
||||
->inject('console')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -48,8 +48,7 @@ class Migrate extends Action
|
||||
Database $dbForPlatform,
|
||||
callable $getProjectDB,
|
||||
Registry $register,
|
||||
Authorization $authorization,
|
||||
Document $console
|
||||
Authorization $authorization
|
||||
): void {
|
||||
|
||||
if (!\array_key_exists($version, Migration::$versions)) {
|
||||
@@ -126,6 +125,8 @@ class Migrate extends Action
|
||||
Console::log('Migrated ' . ++$count . '/' . $total . ' projects...');
|
||||
});
|
||||
|
||||
$console = (new Http('UTC'))->getResource('console');
|
||||
|
||||
try {
|
||||
$migration
|
||||
->setProject($console, $getProjectDB($console), $dbForPlatform, $authorization, $getProjectDB);
|
||||
|
||||
@@ -639,15 +639,29 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
|
||||
// Create or checkout dev branch from the base branch
|
||||
// This ensures dev always starts from the latest base branch,
|
||||
// avoiding history divergence caused by squash merges.
|
||||
// Checkout dev branch (or create if it doesn't exist)
|
||||
try {
|
||||
$repo->execute('checkout', '-B', $gitBranch, $repoBranch);
|
||||
$repo->execute('checkout', '-f', $gitBranch);
|
||||
} catch (\Throwable) {
|
||||
$repo->execute('checkout', '-b', $gitBranch);
|
||||
}
|
||||
|
||||
// Fetch dev branch, or push to create it on remote
|
||||
try {
|
||||
$repo->execute('fetch', 'origin', $gitBranch, '--quiet', '--no-tags', '--depth', '1');
|
||||
} catch (\Throwable) {
|
||||
try {
|
||||
$repo->execute('push', '-u', 'origin', $gitBranch, '--quiet');
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
// Sync with remote dev branch
|
||||
try {
|
||||
$repo->execute('reset', '--hard', "origin/{$gitBranch}");
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
|
||||
// Backup .github before cleaning working tree
|
||||
$githubDir = $target . '/.github';
|
||||
$githubBackup = \sys_get_temp_dir() . '/.github-backup-' . \getmypid();
|
||||
@@ -685,7 +699,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
return true;
|
||||
}
|
||||
|
||||
$repo->execute('push', '--force-with-lease', '-u', 'origin', $gitBranch, '--quiet');
|
||||
$repo->execute('push', '-u', 'origin', $gitBranch, '--quiet');
|
||||
} catch (\Throwable $e) {
|
||||
Console::warning(" Git push failed: " . $e->getMessage());
|
||||
return false;
|
||||
|
||||
@@ -61,7 +61,7 @@ class ScheduleFunctions extends ScheduleBase
|
||||
$nextDate = $cron->getNextRunDate();
|
||||
$next = DateTime::format($nextDate);
|
||||
|
||||
$currentTick = $next < $timeFrame;
|
||||
$currentTick = $next <= $timeFrame;
|
||||
|
||||
if (!$currentTick) {
|
||||
continue;
|
||||
@@ -88,7 +88,7 @@ class ScheduleFunctions extends ScheduleBase
|
||||
$scheduleKey = $delayConfig['key'];
|
||||
// Ensure schedule was not deleted
|
||||
if (!\array_key_exists($scheduleKey, $this->schedules)) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
$schedule = $this->schedules[$scheduleKey];
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Tasks;
|
||||
|
||||
use Appwrite\Network\Validator\Redirect;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Specification\Format\OpenAPI3;
|
||||
@@ -19,9 +18,6 @@ use Utopia\Config\Config;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Adapter\MySQL;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\DI\Container;
|
||||
use Utopia\Http\Adapter\FPM\Server as FPMServer;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Http\Request as UtopiaRequest;
|
||||
use Utopia\Http\Response as UtopiaResponse;
|
||||
@@ -340,17 +336,11 @@ class Specs extends Action
|
||||
|
||||
$mocks = ($mode === 'mocks');
|
||||
|
||||
// Mock dependencies needed by param validator injections in route definitions
|
||||
$specsContainer = new Container();
|
||||
$specsContainer->set('request', fn () => $this->getRequest());
|
||||
$specsContainer->set('response', fn () => $response);
|
||||
$specsContainer->set('dbForPlatform', fn () => new Database(new MySQL(''), new Cache(new None())));
|
||||
$specsContainer->set('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None())));
|
||||
$specsContainer->set('redirectValidator', fn () => new Redirect([], []));
|
||||
$specsContainer->set('project', fn () => new Document([]));
|
||||
$specsContainer->set('passwordsDictionary', fn () => []);
|
||||
$specsContainer->set('localeCodes', fn () => \array_map(fn ($locale) => $locale['code'], Config::getParam('locale-codes', [])));
|
||||
$specsContainer->set('plan', fn () => []);
|
||||
// Mock dependencies
|
||||
Http::setResource('request', fn () => $this->getRequest());
|
||||
Http::setResource('response', fn () => $response);
|
||||
Http::setResource('dbForPlatform', fn () => new Database(new MySQL(''), new Cache(new None())));
|
||||
Http::setResource('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None())));
|
||||
|
||||
$platforms = static::getPlatforms();
|
||||
$authCounts = $this->getAuthCounts();
|
||||
@@ -448,7 +438,7 @@ class Specs extends Action
|
||||
}
|
||||
|
||||
$arguments = [
|
||||
new Http(new FPMServer($specsContainer), 'UTC'),
|
||||
new Http('UTC'),
|
||||
$services,
|
||||
$routes,
|
||||
$models,
|
||||
@@ -482,12 +472,7 @@ class Specs extends Action
|
||||
? $specsDir . '/' . $format . '-mocks-' . $platform . '.json'
|
||||
: $specsDir . '/' . $format . '-' . $version . '-' . $platform . '.json';
|
||||
|
||||
try {
|
||||
$parsedSpecs = $specs->parse();
|
||||
} catch (\RuntimeException $e) {
|
||||
throw new \RuntimeException("Spec generation failed for {$platform} ({$format}): " . $e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
$parsedSpecs = $specs->parse();
|
||||
$encodedSpecs = \json_encode($parsedSpecs, JSON_PRETTY_PRINT);
|
||||
|
||||
unset($parsedSpecs);
|
||||
|
||||
@@ -379,11 +379,7 @@ class Migrations extends Action
|
||||
'webhooks.read',
|
||||
'webhooks.write',
|
||||
'project.read',
|
||||
'project.write',
|
||||
'keys.read',
|
||||
'keys.write',
|
||||
'platforms.read',
|
||||
'platforms.write',
|
||||
'project.write'
|
||||
]
|
||||
]);
|
||||
|
||||
|
||||
@@ -2,14 +2,10 @@
|
||||
|
||||
namespace Appwrite\Promises;
|
||||
|
||||
use Swoole\Coroutine;
|
||||
use Swoole\Coroutine\Channel;
|
||||
use Utopia\DI\Container;
|
||||
|
||||
class Swoole extends Promise
|
||||
{
|
||||
private const REQUEST_CONTAINER_CONTEXT_KEY = '__utopia_http_request_container';
|
||||
|
||||
public function __construct(?callable $executor = null)
|
||||
{
|
||||
parent::__construct($executor);
|
||||
@@ -20,14 +16,7 @@ class Swoole extends Promise
|
||||
callable $resolve,
|
||||
callable $reject
|
||||
): void {
|
||||
$parentContainer = (Coroutine::getCid() !== -1)
|
||||
? (Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] ?? null)
|
||||
: null;
|
||||
|
||||
\go(function () use ($executor, $resolve, $reject, $parentContainer) {
|
||||
if ($parentContainer !== null) {
|
||||
Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] = new Container($parentContainer);
|
||||
}
|
||||
\go(function () use ($executor, $resolve, $reject) {
|
||||
try {
|
||||
$executor($resolve, $reject);
|
||||
} catch (\Throwable $exception) {
|
||||
|
||||
@@ -769,9 +769,6 @@ abstract class Format
|
||||
protected function getNestedModels(Model $model, array &$usedModels): void
|
||||
{
|
||||
foreach ($model->getRules() as $rule) {
|
||||
if (($rule['hidden'] ?? false) === true) {
|
||||
continue;
|
||||
}
|
||||
if (!in_array($model->getType(), $usedModels)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -278,18 +278,6 @@ class OpenAPI3 extends Format
|
||||
}
|
||||
}
|
||||
|
||||
if (\is_string($model)) {
|
||||
throw new \RuntimeException("Unresolved response model '{$model}' for method '{$sdk->getNamespace()}.{$sdk->getMethodName()}'. Ensure the model is registered.");
|
||||
}
|
||||
|
||||
if (\is_array($model)) {
|
||||
foreach ($model as $m) {
|
||||
if (\is_string($m)) {
|
||||
throw new \RuntimeException("Unresolved response model '{$m}' for method '{$sdk->getNamespace()}.{$sdk->getMethodName()}'. Ensure the model is registered.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!(\is_array($model)) && $model->isNone()) {
|
||||
$temp['responses'][(string)$response->getCode() ?? '500'] = [
|
||||
'description' => in_array($produces, [
|
||||
@@ -833,10 +821,6 @@ class OpenAPI3 extends Format
|
||||
}
|
||||
|
||||
foreach ($model->getRules() as $name => $rule) {
|
||||
if (($rule['hidden'] ?? false) === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = '';
|
||||
$format = null;
|
||||
$items = null;
|
||||
|
||||
@@ -285,18 +285,6 @@ class Swagger2 extends Format
|
||||
}
|
||||
}
|
||||
|
||||
if (\is_string($model)) {
|
||||
throw new \RuntimeException("Unresolved response model '{$model}' for method '{$sdk->getNamespace()}.{$sdk->getMethodName()}'. Ensure the model is registered.");
|
||||
}
|
||||
|
||||
if (\is_array($model)) {
|
||||
foreach ($model as $m) {
|
||||
if (\is_string($m)) {
|
||||
throw new \RuntimeException("Unresolved response model '{$m}' for method '{$sdk->getNamespace()}.{$sdk->getMethodName()}'. Ensure the model is registered.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!(\is_array($model)) && $model->isNone()) {
|
||||
$temp['responses'][(string)$response->getCode() ?? '500'] = [
|
||||
'description' => in_array($produces, [
|
||||
@@ -813,10 +801,6 @@ class Swagger2 extends Format
|
||||
}
|
||||
|
||||
foreach ($model->getRules() as $name => $rule) {
|
||||
if (($rule['hidden'] ?? false) === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = '';
|
||||
$format = null;
|
||||
$items = null;
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Database\Validator\Queries;
|
||||
|
||||
class Platforms extends Base
|
||||
{
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
'type',
|
||||
'name',
|
||||
'hostname',
|
||||
'bundleIdentifier',
|
||||
'applicationId',
|
||||
'packageIdentifierName',
|
||||
'packageName',
|
||||
];
|
||||
|
||||
/**
|
||||
* Expression constructor
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('platforms', self::ALLOWED_ATTRIBUTES);
|
||||
}
|
||||
}
|
||||
@@ -11,69 +11,9 @@ class V21 extends Filter
|
||||
public function parse(array $content, string $model): array
|
||||
{
|
||||
switch ($model) {
|
||||
// Web is special case compared to others, because it holds backwards compatibility logic
|
||||
case 'project.createWebPlatform':
|
||||
$content = $this->fillPlatformId($content);
|
||||
$content = $this->removePlatformStore($content);
|
||||
// Keep 'key' for backwards compatibility
|
||||
break;
|
||||
case 'project.updateWebPlatform':
|
||||
$content = $this->removePlatformStore($content);
|
||||
// Keep 'key' for backwards compatibility
|
||||
break;
|
||||
case 'project.createApplePlatform':
|
||||
$content = $this->fillPlatformId($content);
|
||||
$content = $this->removePlatformStore($content);
|
||||
$content = $this->replacePlatformKey($content, 'bundleIdentifier');
|
||||
unset($content['hostname']); // Hostname unsupported
|
||||
break;
|
||||
case 'project.updateApplePlatform':
|
||||
$content = $this->removePlatformStore($content);
|
||||
$content = $this->replacePlatformKey($content, 'bundleIdentifier');
|
||||
unset($content['hostname']); // Hostname unsupported
|
||||
break;
|
||||
case 'project.createAndroidPlatform':
|
||||
$content = $this->fillPlatformId($content);
|
||||
$content = $this->removePlatformStore($content);
|
||||
$content = $this->replacePlatformKey($content, 'applicationId');
|
||||
unset($content['hostname']); // Hostname unsupported
|
||||
break;
|
||||
case 'project.updateAndroidPlatform':
|
||||
$content = $this->removePlatformStore($content);
|
||||
$content = $this->replacePlatformKey($content, 'applicationId');
|
||||
unset($content['hostname']); // Hostname unsupported
|
||||
break;
|
||||
case 'project.createWindowsPlatform':
|
||||
$content = $this->fillPlatformId($content);
|
||||
$content = $this->removePlatformStore($content);
|
||||
$content = $this->replacePlatformKey($content, 'packageIdentifierName');
|
||||
unset($content['hostname']); // Hostname unsupported
|
||||
break;
|
||||
case 'project.updateWindowsPlatform':
|
||||
$content = $this->removePlatformStore($content);
|
||||
$content = $this->replacePlatformKey($content, 'packageIdentifierName');
|
||||
unset($content['hostname']); // Hostname unsupported
|
||||
break;
|
||||
case 'project.createLinuxPlatform':
|
||||
$content = $this->fillPlatformId($content);
|
||||
$content = $this->removePlatformStore($content);
|
||||
$content = $this->replacePlatformKey($content, 'packageName');
|
||||
unset($content['hostname']); // Hostname unsupported
|
||||
break;
|
||||
case 'project.updateLinuxPlatform':
|
||||
$content = $this->removePlatformStore($content);
|
||||
$content = $this->replacePlatformKey($content, 'packageName');
|
||||
unset($content['hostname']); // Hostname unsupported
|
||||
break;
|
||||
case 'project.listPlatforms':
|
||||
$content = $this->preservePlatformsQueries($content);
|
||||
break;
|
||||
case 'webhooks.create':
|
||||
$content = $this->fillWebhookid($content);
|
||||
break;
|
||||
case 'project.createKey':
|
||||
$content = $this->fillKeyId($content);
|
||||
break;
|
||||
case 'project.createVariable':
|
||||
$content = $this->fillVariableId($content);
|
||||
break;
|
||||
@@ -125,12 +65,6 @@ class V21 extends Filter
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function fillKeyId(array $content): array
|
||||
{
|
||||
$content['keyId'] = $content['keyId'] ?? 'unique()';
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function fillVariableId(array $content): array
|
||||
{
|
||||
$content['variableId'] = $content['variableId'] ?? 'unique()';
|
||||
@@ -145,33 +79,4 @@ class V21 extends Filter
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function fillPlatformId(array $content): array
|
||||
{
|
||||
$content['platformId'] = $content['platformId'] ?? 'unique()';
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function replacePlatformKey(array $content, string $newKey): array
|
||||
{
|
||||
$content[$newKey] = $content[$newKey] ?? $content['key'] ?? null;
|
||||
unset($content['key']);
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function removePlatformStore(array $content): array
|
||||
{
|
||||
unset($content['store']);
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function preservePlatformsQueries(array $content): array
|
||||
{
|
||||
$content['queries'] = $content['queries'] ?? [
|
||||
Query::limit(5000)
|
||||
];
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,11 +256,7 @@ class Response extends SwooleResponse
|
||||
public const MODEL_MOCK_NUMBER = 'mockNumber';
|
||||
public const MODEL_AUTH_PROVIDER = 'authProvider';
|
||||
public const MODEL_AUTH_PROVIDER_LIST = 'authProviderList';
|
||||
public const MODEL_PLATFORM_APPLE = 'platformApple';
|
||||
public const MODEL_PLATFORM_ANDROID = 'platformAndroid';
|
||||
public const MODEL_PLATFORM_WINDOWS = 'platformWindows';
|
||||
public const MODEL_PLATFORM_LINUX = 'platformLinux';
|
||||
public const MODEL_PLATFORM_WEB = 'platformWeb';
|
||||
public const MODEL_PLATFORM = 'platform';
|
||||
public const MODEL_PLATFORM_LIST = 'platformList';
|
||||
public const MODEL_VARIABLE = 'variable';
|
||||
public const MODEL_VARIABLE_LIST = 'variableList';
|
||||
@@ -480,13 +476,7 @@ class Response extends SwooleResponse
|
||||
foreach ($rule['type'] as $type) {
|
||||
$condition = false;
|
||||
foreach ($this->getModel($type)->conditions as $attribute => $val) {
|
||||
|
||||
if (\is_array($val)) {
|
||||
$condition = \in_array($item->getAttribute($attribute), $val);
|
||||
} else {
|
||||
$condition = $item->getAttribute($attribute) === $val;
|
||||
}
|
||||
|
||||
$condition = $item->getAttribute($attribute) === $val;
|
||||
if (!$condition) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -11,23 +11,6 @@ class V21 extends Filter
|
||||
public function parse(array $content, string $model): array
|
||||
{
|
||||
return match ($model) {
|
||||
// Web is special case, it has backwards compatibility
|
||||
Response::MODEL_PLATFORM_WEB => $this->parsePlatform($content),
|
||||
Response::MODEL_PLATFORM_APPLE => $this->parsePlatform($content),
|
||||
Response::MODEL_PLATFORM_ANDROID => $this->parsePlatform($content),
|
||||
Response::MODEL_PLATFORM_WINDOWS => $this->parsePlatform($content),
|
||||
Response::MODEL_PLATFORM_LINUX => $this->parsePlatform($content),
|
||||
Response::MODEL_PLATFORM_LIST => $this->handleList(
|
||||
$content,
|
||||
"platforms",
|
||||
fn ($item) => $this->parsePlatform($item),
|
||||
),
|
||||
Response::MODEL_PROJECT => $this->parseProjectForPlatform($content),
|
||||
Response::MODEL_PROJECT_LIST => $this->handleList(
|
||||
$content,
|
||||
"projects",
|
||||
fn ($item) => $this->parseProjectForPlatform($item),
|
||||
),
|
||||
Response::MODEL_USER => $this->parseUser($content),
|
||||
Response::MODEL_USER_LIST => $this->handleList(
|
||||
$content,
|
||||
@@ -124,34 +107,4 @@ class V21 extends Filter
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function parseProjectForPlatform(array $content): array
|
||||
{
|
||||
// Parse platforms under project, since it's a subquery
|
||||
$content['platforms'] = \array_map(fn ($item) => $this->parsePlatform($item), $content['platforms']);
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function parsePlatform(array $content): array
|
||||
{
|
||||
// Map platform-specific identifier fields back to 'key'
|
||||
$content['key'] =
|
||||
($content['bundleIdentifier'] ?? '')
|
||||
?: ($content['applicationId'] ?? '')
|
||||
?: ($content['packageIdentifierName'] ?? '')
|
||||
?: ($content['packageName'] ?? '')
|
||||
?: ($content['key'] ?? '')
|
||||
?: '';
|
||||
|
||||
unset($content['bundleIdentifier']);
|
||||
unset($content['applicationId']);
|
||||
unset($content['packageIdentifierName']);
|
||||
unset($content['packageName']);
|
||||
|
||||
// Restore fields removed in v1.9
|
||||
$content['store'] = $content['store'] ?? '';
|
||||
$content['hostname'] = $content['hostname'] ?? '';
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class AuthProvider extends Model
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $public = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
|
||||
@@ -7,6 +7,11 @@ use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class DevKey extends Model
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $public = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class Platform extends Model
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $public = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('$id', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Platform ID.',
|
||||
'default' => '',
|
||||
'example' => '5e5ea5c16897e',
|
||||
])
|
||||
->addRule('$createdAt', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'Platform creation date in ISO 8601 format.',
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('$updatedAt', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'Platform update date in ISO 8601 format.',
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('name', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Platform name.',
|
||||
'default' => '',
|
||||
'example' => 'My Web App',
|
||||
])
|
||||
->addRule('type', [
|
||||
'type' => self::TYPE_ENUM,
|
||||
'description' => 'Platform type. Possible values are: web, flutter-web, flutter-ios, flutter-android, flutter-linux, flutter-macos, flutter-windows, apple-ios, apple-macos, apple-watchos, apple-tvos, android, unity, react-native-ios, react-native-android.',
|
||||
'default' => '',
|
||||
'example' => 'web',
|
||||
'enum' => ['web', 'flutter-web', 'flutter-ios', 'flutter-android', 'flutter-linux', 'flutter-macos', 'flutter-windows', 'apple-ios', 'apple-macos', 'apple-watchos', 'apple-tvos', 'android', 'unity', 'react-native-ios', 'react-native-android'],
|
||||
])
|
||||
->addRule('key', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Platform Key. iOS bundle ID or Android package name. Empty string for other platforms.',
|
||||
'default' => '',
|
||||
'example' => 'com.company.appname',
|
||||
])
|
||||
->addRule('store', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'App store or Google Play store ID.',
|
||||
'example' => '',
|
||||
])
|
||||
->addRule('hostname', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Web app hostname. Empty string for other platforms.',
|
||||
'default' => '',
|
||||
'example' => 'app.example.com',
|
||||
])
|
||||
->addRule('httpUser', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'HTTP basic authentication username.',
|
||||
'default' => '',
|
||||
'example' => 'username',
|
||||
])
|
||||
->addRule('httpPass', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'HTTP basic authentication password.',
|
||||
'default' => '',
|
||||
'example' => 'password',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Platform';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_PLATFORM;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Document;
|
||||
|
||||
class PlatformAndroid extends PlatformBase
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->conditions = [
|
||||
'type' => Platform::TYPE_ANDROID,
|
||||
];
|
||||
|
||||
parent::__construct();
|
||||
|
||||
$this
|
||||
->addRule('applicationId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Android application ID.',
|
||||
'default' => '',
|
||||
'example' => 'com.company.appname',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Platform Android';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_PLATFORM_ANDROID;
|
||||
}
|
||||
|
||||
public function filter(Document $document): Document
|
||||
{
|
||||
// DB level: 'key'
|
||||
// API level: 'applicationId'
|
||||
$document->setAttribute('applicationId', $document->getAttribute('key', null));
|
||||
$document->removeAttribute('key');
|
||||
|
||||
return $document;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Document;
|
||||
|
||||
class PlatformApple extends PlatformBase
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->conditions = [
|
||||
'type' => Platform::TYPE_APPLE,
|
||||
];
|
||||
|
||||
parent::__construct();
|
||||
|
||||
$this
|
||||
->addRule('bundleIdentifier', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Apple bundle identifier.',
|
||||
'default' => '',
|
||||
'example' => 'com.company.appname',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Platform Apple';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_PLATFORM_APPLE;
|
||||
}
|
||||
|
||||
public function filter(Document $document): Document
|
||||
{
|
||||
// DB level: 'key'
|
||||
// API level: 'bundleIdentifier'
|
||||
$document->setAttribute('bundleIdentifier', $document->getAttribute('key', null));
|
||||
$document->removeAttribute('key');
|
||||
|
||||
return $document;
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
abstract class PlatformBase extends Model
|
||||
{
|
||||
public function getSupportedTypes(): array
|
||||
{
|
||||
return [
|
||||
Platform::TYPE_WINDOWS,
|
||||
Platform::TYPE_APPLE,
|
||||
Platform::TYPE_ANDROID,
|
||||
Platform::TYPE_LINUX,
|
||||
Platform::TYPE_WEB,
|
||||
];
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('$id', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Platform ID.',
|
||||
'default' => '',
|
||||
'example' => '5e5ea5c16897e',
|
||||
])
|
||||
->addRule('$createdAt', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'Platform creation date in ISO 8601 format.',
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('$updatedAt', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'Platform update date in ISO 8601 format.',
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('name', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Platform name.',
|
||||
'default' => '',
|
||||
'example' => 'My Web App',
|
||||
])
|
||||
->addRule('type', [
|
||||
'type' => self::TYPE_ENUM,
|
||||
'description' => 'Platform type. Possible values are: ' . implode(', ', self::getSupportedTypes()) . '.',
|
||||
'default' => '',
|
||||
'example' => Platform::TYPE_WEB,
|
||||
'enum' => self::getSupportedTypes(),
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Document;
|
||||
|
||||
class PlatformLinux extends PlatformBase
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->conditions = [
|
||||
'type' => Platform::TYPE_LINUX,
|
||||
];
|
||||
|
||||
parent::__construct();
|
||||
|
||||
$this
|
||||
->addRule('packageName', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Linux package name.',
|
||||
'default' => '',
|
||||
'example' => 'com.company.appname',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Platform Linux';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_PLATFORM_LINUX;
|
||||
}
|
||||
|
||||
public function filter(Document $document): Document
|
||||
{
|
||||
// DB level: 'key'
|
||||
// API level: 'packageName'
|
||||
$document->setAttribute('packageName', $document->getAttribute('key', null));
|
||||
$document->removeAttribute('key');
|
||||
|
||||
return $document;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class PlatformList extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('total', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Total number of platforms in the given project.',
|
||||
'default' => 0,
|
||||
'example' => 5,
|
||||
])
|
||||
->addRule('platforms', [
|
||||
'type' => [
|
||||
Response::MODEL_PLATFORM_WEB,
|
||||
Response::MODEL_PLATFORM_APPLE,
|
||||
Response::MODEL_PLATFORM_ANDROID,
|
||||
Response::MODEL_PLATFORM_WINDOWS,
|
||||
Response::MODEL_PLATFORM_LINUX,
|
||||
],
|
||||
'description' => 'List of platforms.',
|
||||
'default' => [],
|
||||
'array' => true
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Platforms List';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_PLATFORM_LIST;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\Utopia\Response;
|
||||
|
||||
class PlatformWeb extends PlatformBase
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->conditions = [
|
||||
'type' => [
|
||||
Platform::TYPE_WEB,
|
||||
// Backwards compatibility
|
||||
'flutter-web',
|
||||
'unity',
|
||||
'flutter-macos',
|
||||
'flutter-ios',
|
||||
'react-native-ios',
|
||||
'apple-ios',
|
||||
'apple-macos',
|
||||
'apple-watchos',
|
||||
'apple-tvos',
|
||||
'flutter-android',
|
||||
'react-native-android',
|
||||
'flutter-windows',
|
||||
'flutter-linux',
|
||||
],
|
||||
];
|
||||
|
||||
parent::__construct();
|
||||
|
||||
$this
|
||||
->addRule('hostname', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Web app hostname. Empty string for other platforms.',
|
||||
'default' => '',
|
||||
'example' => 'app.example.com',
|
||||
])
|
||||
// Backwards compatibility
|
||||
->addRule('key', [
|
||||
'hidden' => true,
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Deprecated for old versions using alias endpoint to create universal platform.',
|
||||
'default' => '',
|
||||
'example' => 'com.company.appname',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Platform Web';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_PLATFORM_WEB;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Document;
|
||||
|
||||
class PlatformWindows extends PlatformBase
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->conditions = [
|
||||
'type' => Platform::TYPE_WINDOWS,
|
||||
];
|
||||
|
||||
parent::__construct();
|
||||
|
||||
$this
|
||||
->addRule('packageIdentifierName', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Windows package identifier name.',
|
||||
'default' => '',
|
||||
'example' => 'com.company.appname',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Platform Windows';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_PLATFORM_WINDOWS;
|
||||
}
|
||||
|
||||
public function filter(Document $document): Document
|
||||
{
|
||||
// DB level: 'key'
|
||||
// API level: 'packageIdentifierName'
|
||||
$document->setAttribute('packageIdentifierName', $document->getAttribute('key', null));
|
||||
$document->removeAttribute('key');
|
||||
|
||||
return $document;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,11 @@ use Utopia\Database\Document;
|
||||
|
||||
class Project extends Model
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $public = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
@@ -195,13 +200,7 @@ class Project extends Model
|
||||
'array' => true,
|
||||
])
|
||||
->addRule('platforms', [
|
||||
'type' => [
|
||||
Response::MODEL_PLATFORM_WEB,
|
||||
Response::MODEL_PLATFORM_APPLE,
|
||||
Response::MODEL_PLATFORM_ANDROID,
|
||||
Response::MODEL_PLATFORM_WINDOWS,
|
||||
Response::MODEL_PLATFORM_LINUX,
|
||||
],
|
||||
'type' => Response::MODEL_PLATFORM,
|
||||
'description' => 'List of Platforms.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
|
||||
@@ -7,6 +7,11 @@ use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class Webhook extends Model
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $public = true;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
|
||||
+22
-7
@@ -219,8 +219,7 @@ class Client
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $formattedHeaders);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 120);
|
||||
curl_setopt($ch, CURLOPT_COOKIEFILE, ''); // enable in-memory RFC 6265 cookie engine
|
||||
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) {
|
||||
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders, &$cookies) {
|
||||
$len = strlen($header);
|
||||
$header = explode(':', $header, 2);
|
||||
|
||||
@@ -228,6 +227,12 @@ class Client
|
||||
return $len;
|
||||
}
|
||||
|
||||
if (strtolower(trim($header[0])) == 'set-cookie') {
|
||||
$parsed = $this->parseCookie((string)trim($header[1]));
|
||||
$name = array_key_first($parsed);
|
||||
$cookies[$name] = $parsed[$name];
|
||||
}
|
||||
|
||||
$responseHeaders[strtolower(trim($header[0]))] = trim($header[1]);
|
||||
|
||||
return $len;
|
||||
@@ -254,11 +259,6 @@ class Client
|
||||
$responseType = $responseHeaders['content-type'] ?? '';
|
||||
$responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
foreach (curl_getinfo($ch, CURLINFO_COOKIELIST) as $line) {
|
||||
$parts = explode("\t", $line);
|
||||
$cookies[$parts[5]] = $parts[6] ?? '';
|
||||
}
|
||||
|
||||
if ($decode && $method !== self::METHOD_HEAD) {
|
||||
$strpos = strpos($responseType, ';');
|
||||
$strpos = \is_bool($strpos) ? \strlen($responseType) : $strpos;
|
||||
@@ -309,6 +309,21 @@ class Client
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Cookie String
|
||||
*
|
||||
* @param string $cookie
|
||||
* @return array
|
||||
*/
|
||||
public function parseCookie(string $cookie): array
|
||||
{
|
||||
$cookies = [];
|
||||
|
||||
parse_str(strtr($cookie, ['&' => '%26', '+' => '%2B', ';' => '&']), $cookies);
|
||||
|
||||
return $cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten params array to PHP multiple format
|
||||
*
|
||||
|
||||
@@ -1324,8 +1324,8 @@ class UsageTest extends Scope
|
||||
$this->assertEquals($requestsTotal, $response['body']['requests'][array_key_last($response['body']['requests'])]['value']);
|
||||
$this->validateDates($response['body']['requests']);
|
||||
// vectordbTotal should reflect only VectorsDB instances, not relational databases.
|
||||
$this->assertEquals($vectordbTotal, $response['body']['vectorsdbDatabasesTotal']);
|
||||
$this->assertEquals($documentsTotal, $response['body']['vectorsdbDocumentsTotal']);
|
||||
$this->assertEquals($vectordbTotal, $response['body']['vectordbDatabasesTotal']);
|
||||
$this->assertEquals($documentsTotal, $response['body']['vectordbDocumentsTotal']);
|
||||
});
|
||||
|
||||
$response = $this->client->call(
|
||||
|
||||
@@ -164,11 +164,7 @@ trait ProjectCustom
|
||||
'webhooks.read',
|
||||
'webhooks.write',
|
||||
'project.read',
|
||||
'project.write',
|
||||
'keys.read',
|
||||
'keys.write',
|
||||
'platforms.read',
|
||||
'platforms.write',
|
||||
'project.write'
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
@@ -802,16 +802,6 @@ class AccountCustomClientTest extends Scope
|
||||
$sessionId = $response['body']['$id'];
|
||||
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
|
||||
|
||||
$accountResponse = $this->client->call(Client::METHOD_GET, '/account', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]));
|
||||
|
||||
$this->assertEquals(200, $accountResponse['headers']['status-code']);
|
||||
$this->assertEquals($email, $accountResponse['body']['email']);
|
||||
|
||||
// apiKey is only available in custom client test
|
||||
$apiKey = $this->getProject()['apiKey'];
|
||||
if (!empty($apiKey)) {
|
||||
@@ -4160,178 +4150,4 @@ class AccountCustomClientTest extends Scope
|
||||
|
||||
$this->assertEquals(401, $verification3['headers']['status-code']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a new email/password session is immediately usable even when
|
||||
* a concurrent request re-populates the user cache between the cache purge
|
||||
* and session creation.
|
||||
*
|
||||
* Regression test for: purging the user cache BEFORE persisting the session
|
||||
* allows a concurrent request (from a different Swoole worker) to re-cache
|
||||
* a stale user document that lacks the new session, causing sessionVerify
|
||||
* to fail with 401 on subsequent requests using the new session.
|
||||
*/
|
||||
public function testEmailPasswordSessionNotCorruptedByConcurrentRequests(): void
|
||||
{
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$endpoint = $this->client->getEndpoint();
|
||||
|
||||
$email = uniqid('race_', true) . getmypid() . '@localhost.test';
|
||||
$password = 'password123!';
|
||||
|
||||
// Create user
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], [
|
||||
'userId' => ID::unique(),
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
'name' => 'Race Test User',
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
// Login to get session A
|
||||
$responseA = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], [
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
]);
|
||||
$this->assertEquals(201, $responseA['headers']['status-code']);
|
||||
$sessionA = $responseA['cookies']['a_session_' . $projectId];
|
||||
|
||||
// Verify session A works
|
||||
$verifyA = $this->client->call(Client::METHOD_GET, '/account', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'cookie' => 'a_session_' . $projectId . '=' . $sessionA,
|
||||
]);
|
||||
$this->assertEquals(200, $verifyA['headers']['status-code']);
|
||||
|
||||
/**
|
||||
* Race condition scenario:
|
||||
* 1. Start login B via curl_multi (non-blocking)
|
||||
* 2. Drive the transfer for ~150ms so login B reaches purgeCachedDocument
|
||||
* (findOne ~15ms + Argon2 hash verify ~60ms + middleware overhead)
|
||||
* 3. THEN add GET requests to curl_multi - these hit different workers and
|
||||
* re-cache a stale user document (without session B) during the window
|
||||
* between purgeCachedDocument and createDocument
|
||||
* 4. After all complete, verify session B is usable
|
||||
*/
|
||||
for ($attempt = 0; $attempt < 5; $attempt++) {
|
||||
$loginCookies = [];
|
||||
|
||||
$multi = curl_multi_init();
|
||||
|
||||
// Start login B first (alone)
|
||||
$loginHandle = curl_init("{$endpoint}/account/sessions/email");
|
||||
curl_setopt_array($loginHandle, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'origin: http://localhost',
|
||||
'content-type: application/json',
|
||||
"x-appwrite-project: {$projectId}",
|
||||
],
|
||||
CURLOPT_POSTFIELDS => \json_encode([
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
]),
|
||||
CURLOPT_HEADERFUNCTION => function ($curl, $header) use (&$loginCookies) {
|
||||
if (\stripos($header, 'set-cookie:') === 0) {
|
||||
$cookiePart = \trim(\substr($header, 11));
|
||||
$eqPos = \strpos($cookiePart, '=');
|
||||
if ($eqPos !== false) {
|
||||
$name = \substr($cookiePart, 0, $eqPos);
|
||||
$rest = \substr($cookiePart, $eqPos + 1);
|
||||
$semiPos = \strpos($rest, ';');
|
||||
$loginCookies[$name] = $semiPos !== false
|
||||
? \substr($rest, 0, $semiPos)
|
||||
: $rest;
|
||||
}
|
||||
}
|
||||
return \strlen($header);
|
||||
},
|
||||
]);
|
||||
curl_multi_add_handle($multi, $loginHandle);
|
||||
|
||||
// Drive the login transfer forward and wait for the server to start
|
||||
// processing the login (past hash verification + cache purge).
|
||||
$deadline = \microtime(true) + 0.15; // 150ms
|
||||
do {
|
||||
curl_multi_exec($multi, $active);
|
||||
curl_multi_select($multi, 0.005);
|
||||
} while (\microtime(true) < $deadline && $active);
|
||||
|
||||
// NOW add GET requests - they arrive after the cache purge
|
||||
// but before session creation (which is delayed by the usleep or I/O).
|
||||
$getHandles = [];
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$gh = curl_init("{$endpoint}/account");
|
||||
curl_setopt_array($gh, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'origin: http://localhost',
|
||||
'content-type: application/json',
|
||||
"x-appwrite-project: {$projectId}",
|
||||
"cookie: a_session_{$projectId}={$sessionA}",
|
||||
],
|
||||
]);
|
||||
curl_multi_add_handle($multi, $gh);
|
||||
$getHandles[] = $gh;
|
||||
}
|
||||
|
||||
// Drive all to completion
|
||||
do {
|
||||
$status = curl_multi_exec($multi, $active);
|
||||
if ($active) {
|
||||
curl_multi_select($multi, 0.05);
|
||||
}
|
||||
} while ($active && $status === CURLM_OK);
|
||||
|
||||
$loginStatus = curl_getinfo($loginHandle, CURLINFO_HTTP_CODE);
|
||||
|
||||
curl_multi_remove_handle($multi, $loginHandle);
|
||||
curl_close($loginHandle);
|
||||
foreach ($getHandles as $gh) {
|
||||
curl_multi_remove_handle($multi, $gh);
|
||||
curl_close($gh);
|
||||
}
|
||||
curl_multi_close($multi);
|
||||
|
||||
$this->assertEquals(201, $loginStatus, 'Login for session B should succeed');
|
||||
|
||||
$sessionBCookie = $loginCookies["a_session_{$projectId}"] ?? null;
|
||||
$this->assertNotNull($sessionBCookie, 'Session B cookie should be set');
|
||||
|
||||
// THE CRITICAL CHECK: verify session B is usable immediately
|
||||
$verifyB = $this->client->call(Client::METHOD_GET, '/account', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'cookie' => "a_session_{$projectId}={$sessionBCookie}",
|
||||
]);
|
||||
|
||||
$this->assertEquals(
|
||||
200,
|
||||
$verifyB['headers']['status-code'],
|
||||
'Session B must be immediately usable after login. '
|
||||
. 'A 401 here means a stale user cache (without the new session) was served. '
|
||||
. 'The fix is to create the session document BEFORE purging the user cache.'
|
||||
);
|
||||
|
||||
// Clean up session B for next iteration
|
||||
$this->client->call(Client::METHOD_DELETE, '/account/sessions/current', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'cookie' => "a_session_{$projectId}={$sessionBCookie}",
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,806 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Project;
|
||||
|
||||
use Appwrite\Tests\Async;
|
||||
use Tests\E2E\Client;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
|
||||
trait KeysBase
|
||||
{
|
||||
use Async;
|
||||
|
||||
// =========================================================================
|
||||
// Create key tests
|
||||
// =========================================================================
|
||||
|
||||
public function testCreateKey(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'My API Key',
|
||||
['users.read', 'users.write'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$this->assertNotEmpty($key['body']['$id']);
|
||||
$this->assertSame('My API Key', $key['body']['name']);
|
||||
$this->assertSame(['users.read', 'users.write'], $key['body']['scopes']);
|
||||
$this->assertNotEmpty($key['body']['secret']);
|
||||
$this->assertSame('', $key['body']['expire']);
|
||||
$this->assertSame('', $key['body']['accessedAt']);
|
||||
$this->assertSame([], $key['body']['sdks']);
|
||||
|
||||
$dateValidator = new DatetimeValidator();
|
||||
$this->assertSame(true, $dateValidator->isValid($key['body']['$createdAt']));
|
||||
$this->assertSame(true, $dateValidator->isValid($key['body']['$updatedAt']));
|
||||
|
||||
// Verify via GET
|
||||
$get = $this->getKey($key['body']['$id']);
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
$this->assertSame($key['body']['$id'], $get['body']['$id']);
|
||||
$this->assertSame('My API Key', $get['body']['name']);
|
||||
$this->assertSame(['users.read', 'users.write'], $get['body']['scopes']);
|
||||
|
||||
// Verify via LIST
|
||||
$list = $this->listKeys(null, true);
|
||||
$this->assertSame(200, $list['headers']['status-code']);
|
||||
$this->assertGreaterThanOrEqual(1, $list['body']['total']);
|
||||
$this->assertGreaterThanOrEqual(1, \count($list['body']['keys']));
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($key['body']['$id']);
|
||||
}
|
||||
|
||||
public function testCreateKeyWithExpire(): void
|
||||
{
|
||||
$expire = '2030-01-01T00:00:00.000+00:00';
|
||||
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Expiring Key',
|
||||
['users.read'],
|
||||
$expire,
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$this->assertSame($expire, $key['body']['expire']);
|
||||
|
||||
// Verify via GET
|
||||
$get = $this->getKey($key['body']['$id']);
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
$this->assertSame($expire, $get['body']['expire']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($key['body']['$id']);
|
||||
}
|
||||
|
||||
public function testCreateKeyWithNullScopes(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Null Scopes Key',
|
||||
null,
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$this->assertSame([], $key['body']['scopes']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($key['body']['$id']);
|
||||
}
|
||||
|
||||
public function testCreateKeyWithoutAuthentication(): void
|
||||
{
|
||||
$response = $this->createKey(
|
||||
ID::unique(),
|
||||
'No Auth Key',
|
||||
['users.read'],
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
$this->assertSame(401, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testCreateKeyInvalidId(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
'!invalid-id!',
|
||||
'Invalid ID Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(400, $key['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testCreateKeyMissingName(): void
|
||||
{
|
||||
$response = $this->createKey(
|
||||
ID::unique(),
|
||||
null,
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testCreateKeyInvalidScope(): void
|
||||
{
|
||||
$response = $this->createKey(
|
||||
ID::unique(),
|
||||
'Invalid Scope Key',
|
||||
['invalid.scope'],
|
||||
);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testCreateKeyDuplicateId(): void
|
||||
{
|
||||
$keyId = ID::unique();
|
||||
|
||||
$key = $this->createKey(
|
||||
$keyId,
|
||||
'Key Dup 1',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
|
||||
// Attempt to create with same ID
|
||||
$duplicate = $this->createKey(
|
||||
$keyId,
|
||||
'Key Dup 2',
|
||||
['users.write'],
|
||||
);
|
||||
|
||||
$this->assertSame(409, $duplicate['headers']['status-code']);
|
||||
$this->assertSame('key_already_exists', $duplicate['body']['type']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
public function testCreateKeyCustomId(): void
|
||||
{
|
||||
$customId = 'my-custom-key-id';
|
||||
|
||||
$key = $this->createKey(
|
||||
$customId,
|
||||
'Custom ID Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$this->assertSame($customId, $key['body']['$id']);
|
||||
|
||||
// Verify via GET
|
||||
$get = $this->getKey($customId);
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
$this->assertSame($customId, $get['body']['$id']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($customId);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Update key tests
|
||||
// =========================================================================
|
||||
|
||||
public function testUpdateKey(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Original Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
// Update name, scopes, and expire
|
||||
$expire = '2031-06-15T12:00:00.000+00:00';
|
||||
$updated = $this->updateKey($keyId, 'Updated Key', ['users.write', 'databases.read'], $expire);
|
||||
|
||||
$this->assertSame(200, $updated['headers']['status-code']);
|
||||
$this->assertSame($keyId, $updated['body']['$id']);
|
||||
$this->assertSame('Updated Key', $updated['body']['name']);
|
||||
$this->assertSame(['users.write', 'databases.read'], $updated['body']['scopes']);
|
||||
$this->assertSame($expire, $updated['body']['expire']);
|
||||
|
||||
// Verify update persisted via GET
|
||||
$get = $this->getKey($keyId);
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
$this->assertSame('Updated Key', $get['body']['name']);
|
||||
$this->assertSame(['users.write', 'databases.read'], $get['body']['scopes']);
|
||||
$this->assertSame($expire, $get['body']['expire']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
public function testUpdateKeyName(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Name Before',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
$updated = $this->updateKey($keyId, 'Name After', ['users.read']);
|
||||
|
||||
$this->assertSame(200, $updated['headers']['status-code']);
|
||||
$this->assertSame('Name After', $updated['body']['name']);
|
||||
$this->assertSame(['users.read'], $updated['body']['scopes']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
public function testUpdateKeyScopes(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Scopes Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
$updated = $this->updateKey($keyId, 'Scopes Key', ['databases.read', 'databases.write']);
|
||||
|
||||
$this->assertSame(200, $updated['headers']['status-code']);
|
||||
$this->assertSame(['databases.read', 'databases.write'], $updated['body']['scopes']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
public function testUpdateKeySetExpire(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'No Expire Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$this->assertSame('', $key['body']['expire']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
$expire = '2032-12-31T23:59:59.000+00:00';
|
||||
$updated = $this->updateKey($keyId, 'No Expire Key', ['users.read'], $expire);
|
||||
|
||||
$this->assertSame(200, $updated['headers']['status-code']);
|
||||
$this->assertSame($expire, $updated['body']['expire']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
public function testUpdateKeyRemoveExpire(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Expire Key',
|
||||
['users.read'],
|
||||
'2030-01-01T00:00:00.000+00:00',
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
// Remove expire by setting to null
|
||||
$updated = $this->updateKey($keyId, 'Expire Key', ['users.read'], null);
|
||||
|
||||
$this->assertSame(200, $updated['headers']['status-code']);
|
||||
$this->assertSame('', $updated['body']['expire']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
public function testUpdateKeyWithoutAuthentication(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Auth Update Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
// Attempt update without authentication
|
||||
$response = $this->updateKey($keyId, 'Updated Name', ['users.read'], null, false);
|
||||
|
||||
$this->assertSame(401, $response['headers']['status-code']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
public function testUpdateKeyNotFound(): void
|
||||
{
|
||||
$updated = $this->updateKey('non-existent-id', 'New Name', ['users.read']);
|
||||
|
||||
$this->assertSame(404, $updated['headers']['status-code']);
|
||||
$this->assertSame('key_not_found', $updated['body']['type']);
|
||||
}
|
||||
|
||||
public function testUpdateKeyInvalidScope(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Invalid Scope Update',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
$updated = $this->updateKey($keyId, 'Invalid Scope Update', ['invalid.scope']);
|
||||
|
||||
$this->assertSame(400, $updated['headers']['status-code']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Get key tests
|
||||
// =========================================================================
|
||||
|
||||
public function testGetKey(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Get Test Key',
|
||||
['users.read', 'databases.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
$get = $this->getKey($keyId);
|
||||
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
$this->assertSame($keyId, $get['body']['$id']);
|
||||
$this->assertSame('Get Test Key', $get['body']['name']);
|
||||
$this->assertSame(['users.read', 'databases.read'], $get['body']['scopes']);
|
||||
$this->assertNotEmpty($get['body']['secret']);
|
||||
$this->assertSame('', $get['body']['expire']);
|
||||
$this->assertSame('', $get['body']['accessedAt']);
|
||||
$this->assertSame([], $get['body']['sdks']);
|
||||
|
||||
$dateValidator = new DatetimeValidator();
|
||||
$this->assertSame(true, $dateValidator->isValid($get['body']['$createdAt']));
|
||||
$this->assertSame(true, $dateValidator->isValid($get['body']['$updatedAt']));
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
public function testGetKeyNotFound(): void
|
||||
{
|
||||
$get = $this->getKey('non-existent-id');
|
||||
|
||||
$this->assertSame(404, $get['headers']['status-code']);
|
||||
$this->assertSame('key_not_found', $get['body']['type']);
|
||||
}
|
||||
|
||||
public function testGetKeyWithoutAuthentication(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Auth Get Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
// Attempt GET without authentication
|
||||
$response = $this->getKey($keyId, false);
|
||||
|
||||
$this->assertSame(401, $response['headers']['status-code']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// List keys tests
|
||||
// =========================================================================
|
||||
|
||||
public function testListKeys(): void
|
||||
{
|
||||
// Create multiple keys
|
||||
$key1 = $this->createKey(
|
||||
ID::unique(),
|
||||
'List Key Alpha',
|
||||
['users.read'],
|
||||
);
|
||||
$this->assertSame(201, $key1['headers']['status-code']);
|
||||
|
||||
$key2 = $this->createKey(
|
||||
ID::unique(),
|
||||
'List Key Beta',
|
||||
['databases.read'],
|
||||
);
|
||||
$this->assertSame(201, $key2['headers']['status-code']);
|
||||
|
||||
$key3 = $this->createKey(
|
||||
ID::unique(),
|
||||
'List Key Gamma',
|
||||
['users.write'],
|
||||
);
|
||||
$this->assertSame(201, $key3['headers']['status-code']);
|
||||
|
||||
// List all
|
||||
$list = $this->listKeys(null, true);
|
||||
|
||||
$this->assertSame(200, $list['headers']['status-code']);
|
||||
$this->assertGreaterThanOrEqual(3, $list['body']['total']);
|
||||
$this->assertGreaterThanOrEqual(3, \count($list['body']['keys']));
|
||||
$this->assertIsArray($list['body']['keys']);
|
||||
|
||||
// Verify structure of returned keys
|
||||
foreach ($list['body']['keys'] as $key) {
|
||||
$this->assertArrayHasKey('$id', $key);
|
||||
$this->assertArrayHasKey('$createdAt', $key);
|
||||
$this->assertArrayHasKey('$updatedAt', $key);
|
||||
$this->assertArrayHasKey('name', $key);
|
||||
$this->assertArrayHasKey('scopes', $key);
|
||||
$this->assertArrayHasKey('secret', $key);
|
||||
$this->assertArrayHasKey('expire', $key);
|
||||
$this->assertArrayHasKey('accessedAt', $key);
|
||||
$this->assertArrayHasKey('sdks', $key);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($key1['body']['$id']);
|
||||
$this->deleteKey($key2['body']['$id']);
|
||||
$this->deleteKey($key3['body']['$id']);
|
||||
}
|
||||
|
||||
public function testListKeysWithLimit(): void
|
||||
{
|
||||
$key1 = $this->createKey(
|
||||
ID::unique(),
|
||||
'Limit Key 1',
|
||||
['users.read'],
|
||||
);
|
||||
$this->assertSame(201, $key1['headers']['status-code']);
|
||||
|
||||
$key2 = $this->createKey(
|
||||
ID::unique(),
|
||||
'Limit Key 2',
|
||||
['users.write'],
|
||||
);
|
||||
$this->assertSame(201, $key2['headers']['status-code']);
|
||||
|
||||
// List with limit 1
|
||||
$list = $this->listKeys([
|
||||
Query::limit(1)->toString(),
|
||||
], true);
|
||||
|
||||
$this->assertSame(200, $list['headers']['status-code']);
|
||||
$this->assertCount(1, $list['body']['keys']);
|
||||
$this->assertGreaterThanOrEqual(2, $list['body']['total']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($key1['body']['$id']);
|
||||
$this->deleteKey($key2['body']['$id']);
|
||||
}
|
||||
|
||||
public function testListKeysWithoutTotal(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'No Total Key',
|
||||
['users.read'],
|
||||
);
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
|
||||
// List with total=false
|
||||
$list = $this->listKeys(null, false);
|
||||
|
||||
$this->assertSame(200, $list['headers']['status-code']);
|
||||
$this->assertSame(0, $list['body']['total']);
|
||||
$this->assertGreaterThanOrEqual(1, \count($list['body']['keys']));
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($key['body']['$id']);
|
||||
}
|
||||
|
||||
public function testListKeysCursorPagination(): void
|
||||
{
|
||||
$key1 = $this->createKey(
|
||||
ID::unique(),
|
||||
'Cursor Key 1',
|
||||
['users.read'],
|
||||
);
|
||||
$this->assertSame(201, $key1['headers']['status-code']);
|
||||
|
||||
$key2 = $this->createKey(
|
||||
ID::unique(),
|
||||
'Cursor Key 2',
|
||||
['users.write'],
|
||||
);
|
||||
$this->assertSame(201, $key2['headers']['status-code']);
|
||||
|
||||
// Get first page with limit 1
|
||||
$page1 = $this->listKeys([
|
||||
Query::limit(1)->toString(),
|
||||
], true);
|
||||
|
||||
$this->assertSame(200, $page1['headers']['status-code']);
|
||||
$this->assertCount(1, $page1['body']['keys']);
|
||||
$cursorId = $page1['body']['keys'][0]['$id'];
|
||||
|
||||
// Get next page using cursor
|
||||
$page2 = $this->listKeys([
|
||||
Query::limit(1)->toString(),
|
||||
Query::cursorAfter(new Document(['$id' => $cursorId]))->toString(),
|
||||
], true);
|
||||
|
||||
$this->assertSame(200, $page2['headers']['status-code']);
|
||||
$this->assertCount(1, $page2['body']['keys']);
|
||||
$this->assertNotEquals($cursorId, $page2['body']['keys'][0]['$id']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($key1['body']['$id']);
|
||||
$this->deleteKey($key2['body']['$id']);
|
||||
}
|
||||
|
||||
public function testListKeysWithoutAuthentication(): void
|
||||
{
|
||||
$response = $this->listKeys(null, null, false);
|
||||
|
||||
$this->assertSame(401, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testListKeysInvalidCursor(): void
|
||||
{
|
||||
$list = $this->listKeys([
|
||||
Query::cursorAfter(new Document(['$id' => 'non-existent-id']))->toString(),
|
||||
], true);
|
||||
|
||||
$this->assertSame(400, $list['headers']['status-code']);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Delete key tests
|
||||
// =========================================================================
|
||||
|
||||
public function testDeleteKey(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Delete Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
// Verify it exists
|
||||
$get = $this->getKey($keyId);
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
|
||||
// Delete
|
||||
$delete = $this->deleteKey($keyId);
|
||||
$this->assertSame(204, $delete['headers']['status-code']);
|
||||
$this->assertEmpty($delete['body']);
|
||||
|
||||
// Verify it no longer exists
|
||||
$get = $this->getKey($keyId);
|
||||
$this->assertSame(404, $get['headers']['status-code']);
|
||||
$this->assertSame('key_not_found', $get['body']['type']);
|
||||
}
|
||||
|
||||
public function testDeleteKeyNotFound(): void
|
||||
{
|
||||
$delete = $this->deleteKey('non-existent-id');
|
||||
|
||||
$this->assertSame(404, $delete['headers']['status-code']);
|
||||
$this->assertSame('key_not_found', $delete['body']['type']);
|
||||
}
|
||||
|
||||
public function testDeleteKeyWithoutAuthentication(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Delete Auth Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
// Attempt DELETE without authentication
|
||||
$response = $this->deleteKey($keyId, false);
|
||||
|
||||
$this->assertSame(401, $response['headers']['status-code']);
|
||||
|
||||
// Verify it still exists
|
||||
$get = $this->getKey($keyId);
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
public function testDeleteKeyRemovedFromList(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Delete List Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
// Get list count before delete
|
||||
$listBefore = $this->listKeys(null, true);
|
||||
$this->assertSame(200, $listBefore['headers']['status-code']);
|
||||
$countBefore = $listBefore['body']['total'];
|
||||
|
||||
// Delete
|
||||
$delete = $this->deleteKey($keyId);
|
||||
$this->assertSame(204, $delete['headers']['status-code']);
|
||||
|
||||
// Get list count after delete
|
||||
$listAfter = $this->listKeys(null, true);
|
||||
$this->assertSame(200, $listAfter['headers']['status-code']);
|
||||
$this->assertSame($countBefore - 1, $listAfter['body']['total']);
|
||||
|
||||
// Verify the deleted key is not in the list
|
||||
$ids = \array_column($listAfter['body']['keys'], '$id');
|
||||
$this->assertNotContains($keyId, $ids);
|
||||
}
|
||||
|
||||
public function testDeleteKeyDoubleDelete(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Double Delete Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
// First delete succeeds
|
||||
$delete = $this->deleteKey($keyId);
|
||||
$this->assertSame(204, $delete['headers']['status-code']);
|
||||
|
||||
// Second delete returns 404
|
||||
$delete = $this->deleteKey($keyId);
|
||||
$this->assertSame(404, $delete['headers']['status-code']);
|
||||
$this->assertSame('key_not_found', $delete['body']['type']);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helpers
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @param array<string>|null $scopes
|
||||
*/
|
||||
protected function createKey(string $keyId, ?string $name, ?array $scopes = null, ?string $expire = null, bool $authenticated = true): mixed
|
||||
{
|
||||
$params = [
|
||||
'keyId' => $keyId,
|
||||
'scopes' => $scopes,
|
||||
];
|
||||
|
||||
if ($name !== null) {
|
||||
$params['name'] = $name;
|
||||
}
|
||||
|
||||
if ($expire !== null) {
|
||||
$params['expire'] = $expire;
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
];
|
||||
|
||||
if ($authenticated) {
|
||||
$headers = array_merge($headers, $this->getHeaders());
|
||||
}
|
||||
|
||||
return $this->client->call(Client::METHOD_POST, '/project/keys', $headers, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string>|null $scopes
|
||||
*/
|
||||
protected function updateKey(string $keyId, ?string $name = null, ?array $scopes = null, ?string $expire = null, bool $authenticated = true): mixed
|
||||
{
|
||||
$params = [];
|
||||
|
||||
if ($name !== null) {
|
||||
$params['name'] = $name;
|
||||
}
|
||||
|
||||
if ($scopes !== null) {
|
||||
$params['scopes'] = $scopes;
|
||||
}
|
||||
|
||||
if ($expire !== null) {
|
||||
$params['expire'] = $expire;
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
];
|
||||
|
||||
if ($authenticated) {
|
||||
$headers = array_merge($headers, $this->getHeaders());
|
||||
}
|
||||
|
||||
return $this->client->call(Client::METHOD_PUT, '/project/keys/' . $keyId, $headers, $params);
|
||||
}
|
||||
|
||||
protected function getKey(string $keyId, bool $authenticated = true): mixed
|
||||
{
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
];
|
||||
|
||||
if ($authenticated) {
|
||||
$headers = array_merge($headers, $this->getHeaders());
|
||||
}
|
||||
|
||||
return $this->client->call(Client::METHOD_GET, '/project/keys/' . $keyId, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string>|null $queries
|
||||
*/
|
||||
protected function listKeys(?array $queries, ?bool $total, bool $authenticated = true): mixed
|
||||
{
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
];
|
||||
|
||||
if ($authenticated) {
|
||||
$headers = array_merge($headers, $this->getHeaders());
|
||||
}
|
||||
|
||||
return $this->client->call(Client::METHOD_GET, '/project/keys', $headers, [
|
||||
'queries' => $queries,
|
||||
'total' => $total,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function deleteKey(string $keyId, bool $authenticated = true): mixed
|
||||
{
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
];
|
||||
|
||||
if ($authenticated) {
|
||||
$headers = array_merge($headers, $this->getHeaders());
|
||||
}
|
||||
|
||||
return $this->client->call(Client::METHOD_DELETE, '/project/keys/' . $keyId, $headers);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Project;
|
||||
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideConsole;
|
||||
|
||||
class KeysConsoleClientTest extends Scope
|
||||
{
|
||||
use KeysBase;
|
||||
use ProjectCustom;
|
||||
use SideConsole;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Project;
|
||||
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideServer;
|
||||
|
||||
class KeysCustomServerTest extends Scope
|
||||
{
|
||||
use KeysBase;
|
||||
use ProjectCustom;
|
||||
use SideServer;
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Project;
|
||||
|
||||
trait LabelsBase
|
||||
{
|
||||
// Update labels tests
|
||||
|
||||
public function testUpdateLabels(): void
|
||||
{
|
||||
$response = $this->updateLabels(['frontend', 'backend']);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertIsArray($response['body']['labels']);
|
||||
$this->assertCount(2, $response['body']['labels']);
|
||||
$this->assertContains('frontend', $response['body']['labels']);
|
||||
$this->assertContains('backend', $response['body']['labels']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateLabels([]);
|
||||
}
|
||||
|
||||
public function testUpdateLabelsReplace(): void
|
||||
{
|
||||
$response = $this->updateLabels(['alpha', 'beta']);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertCount(2, $response['body']['labels']);
|
||||
$this->assertContains('alpha', $response['body']['labels']);
|
||||
$this->assertContains('beta', $response['body']['labels']);
|
||||
|
||||
// Replace with new labels
|
||||
$response = $this->updateLabels(['gamma', 'delta', 'epsilon']);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertCount(3, $response['body']['labels']);
|
||||
$this->assertContains('gamma', $response['body']['labels']);
|
||||
$this->assertContains('delta', $response['body']['labels']);
|
||||
$this->assertContains('epsilon', $response['body']['labels']);
|
||||
$this->assertNotContains('alpha', $response['body']['labels']);
|
||||
$this->assertNotContains('beta', $response['body']['labels']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateLabels([]);
|
||||
}
|
||||
|
||||
public function testUpdateLabelsEmpty(): void
|
||||
{
|
||||
// Set some labels first
|
||||
$response = $this->updateLabels(['toRemove']);
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertCount(1, $response['body']['labels']);
|
||||
|
||||
// Clear all labels
|
||||
$response = $this->updateLabels([]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertIsArray($response['body']['labels']);
|
||||
$this->assertCount(0, $response['body']['labels']);
|
||||
}
|
||||
|
||||
public function testUpdateLabelsDeduplicated(): void
|
||||
{
|
||||
$response = $this->updateLabels(['duplicate', 'duplicate', 'unique']);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertCount(2, $response['body']['labels']);
|
||||
$this->assertContains('duplicate', $response['body']['labels']);
|
||||
$this->assertContains('unique', $response['body']['labels']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateLabels([]);
|
||||
}
|
||||
|
||||
public function testUpdateLabelsSingleLabel(): void
|
||||
{
|
||||
$response = $this->updateLabels(['solo']);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertCount(1, $response['body']['labels']);
|
||||
$this->assertContains('solo', $response['body']['labels']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateLabels([]);
|
||||
}
|
||||
|
||||
public function testUpdateLabelsWithoutAuthentication(): void
|
||||
{
|
||||
$response = $this->updateLabels(['unauthorized'], false);
|
||||
|
||||
$this->assertSame(401, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testUpdateLabelsInvalidLabelTooLong(): void
|
||||
{
|
||||
$response = $this->updateLabels([str_repeat('a', 37)]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testUpdateLabelsInvalidLabelCharacters(): void
|
||||
{
|
||||
$response = $this->updateLabels(['invalid-label!']);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testUpdateLabelsAlphanumericOnly(): void
|
||||
{
|
||||
$response = $this->updateLabels(['ABC123', 'lowercase', 'UPPERCASE', '0123456789']);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertCount(4, $response['body']['labels']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateLabels([]);
|
||||
}
|
||||
|
||||
public function testUpdateLabelsMaxLength(): void
|
||||
{
|
||||
$label = str_repeat('a', 36);
|
||||
$response = $this->updateLabels([$label]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertCount(1, $response['body']['labels']);
|
||||
$this->assertContains($label, $response['body']['labels']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateLabels([]);
|
||||
}
|
||||
|
||||
public function testUpdateLabelsIdempotent(): void
|
||||
{
|
||||
$labels = ['stable', 'production'];
|
||||
|
||||
$first = $this->updateLabels($labels);
|
||||
$this->assertSame(200, $first['headers']['status-code']);
|
||||
|
||||
$second = $this->updateLabels($labels);
|
||||
$this->assertSame(200, $second['headers']['status-code']);
|
||||
|
||||
$this->assertSame($first['body']['labels'], $second['body']['labels']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateLabels([]);
|
||||
}
|
||||
|
||||
public function testUpdateLabelsDeduplicatedOrder(): void
|
||||
{
|
||||
$response = $this->updateLabels(['b', 'a', 'b']);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertCount(2, $response['body']['labels']);
|
||||
$this->assertSame('b', $response['body']['labels'][0]);
|
||||
$this->assertSame('a', $response['body']['labels'][1]);
|
||||
|
||||
// Cleanup
|
||||
$this->updateLabels([]);
|
||||
}
|
||||
|
||||
public function testUpdateLabelsInvalidHyphen(): void
|
||||
{
|
||||
$response = $this->updateLabels(['my-label']);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testUpdateLabelsInvalidUnderscore(): void
|
||||
{
|
||||
$response = $this->updateLabels(['my_label']);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testUpdateLabelsInvalidSpace(): void
|
||||
{
|
||||
$response = $this->updateLabels(['my label']);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testUpdateLabelsInvalidEmptyString(): void
|
||||
{
|
||||
$response = $this->updateLabels(['']);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testUpdateLabelsResponseModel(): void
|
||||
{
|
||||
$response = $this->updateLabels(['test']);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertArrayHasKey('$id', $response['body']);
|
||||
$this->assertArrayHasKey('name', $response['body']);
|
||||
$this->assertArrayHasKey('labels', $response['body']);
|
||||
$this->assertIsArray($response['body']['labels']);
|
||||
$this->assertContains('test', $response['body']['labels']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateLabels([]);
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
/**
|
||||
* @param array<string> $labels
|
||||
*/
|
||||
protected function updateLabels(array $labels, bool $authenticated = true): mixed
|
||||
{
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
];
|
||||
|
||||
if ($authenticated) {
|
||||
$headers = array_merge($headers, $this->getHeaders());
|
||||
}
|
||||
|
||||
return $this->client->call(\Tests\E2E\Client::METHOD_PUT, '/project/labels', $headers, [
|
||||
'labels' => $labels,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Project;
|
||||
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideConsole;
|
||||
|
||||
class LabelsConsoleClientTest extends Scope
|
||||
{
|
||||
use LabelsBase;
|
||||
use ProjectCustom;
|
||||
use SideConsole;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Project;
|
||||
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideServer;
|
||||
|
||||
class LabelsCustomServerTest extends Scope
|
||||
{
|
||||
use LabelsBase;
|
||||
use ProjectCustom;
|
||||
use SideServer;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user