Merge remote-tracking branch 'refs/remotes/origin/main' into feat-eldad4-coroutines

# Conflicts:
#	app/cli.php
#	app/console
#	app/controllers/api/projects.php
#	app/controllers/general.php
#	app/init.php
#	app/realtime.php
#	app/worker.php
#	composer.json
#	composer.lock
This commit is contained in:
Binyamin Yawitz
2024-07-03 17:03:24 -04:00
29 changed files with 2200 additions and 2858 deletions
+3
View File
@@ -145,3 +145,6 @@ jobs:
- name: Run ${{matrix.service}} Tests
run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug
- name: Run ${{matrix.service}} Shared Tables Tests
run: _APP_DATABASE_SHARED_TABLES=database_db_main docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug
+1 -1
View File
@@ -1,4 +1,4 @@
[submodule "app/console"]
path = app/console
url = https://github.com/appwrite/console
branch = 4.3.5
branch = 4.3.14
+9
View File
@@ -29,6 +29,7 @@ $context = new Dependency();
$register = new Dependency();
$logError = new Dependency();
$queueForDeletes = new Dependency();
$queueForFunctions = new Dependency();
$queueForCertificates = new Dependency();
$context
@@ -41,6 +42,13 @@ $register
return $global;
});
$queueForFunctions
->setName('queueForFunctions')
->inject('queue')
->setCallback(function (Connection $queue) {
return new Func($queue);
});
$queueForDeletes
->setName('queueForDeletes')
@@ -100,6 +108,7 @@ $container->set($context);
$container->set($logError);
$container->set($register);
$container->set($queueForDeletes);
$container->set($queueForFunctions);
$container->set($queueForCertificates);
$platform = new Appwrite();
+3 -3
View File
@@ -15,7 +15,7 @@ return [
[
'key' => 'web',
'name' => 'Web',
'version' => '14.0.2',
'version' => '15.0.0',
'url' => 'https://github.com/appwrite/sdk-for-web',
'package' => 'https://www.npmjs.com/package/appwrite',
'enabled' => true,
@@ -138,7 +138,7 @@ return [
[
'key' => 'react-native',
'name' => 'React Native',
'version' => '0.3.2',
'version' => '0.4.0',
'url' => 'https://github.com/appwrite/sdk-for-react-native',
'package' => 'https://npmjs.com/package/react-native-appwrite',
'enabled' => true,
@@ -267,7 +267,7 @@ return [
[
'key' => 'deno',
'name' => 'Deno',
'version' => '10.0.2',
'version' => '11.0.0',
'url' => 'https://github.com/appwrite/sdk-for-deno',
'package' => 'https://deno.land/x/appwrite',
'enabled' => true,
+1 -1
View File
@@ -468,7 +468,7 @@ return [
],
[
'name' => '_APP_SMS_FROM',
'description' => 'Phone number used for sending out messages. Must start with a leading \'+\' and maximum of 15 digits without spaces (+123456789).',
'description' => 'Phone number used for sending out messages. If using Twilio, this may be a Messaging Service SID, starting with MG. Otherwise, the number must start with a leading \'+\' and maximum of 15 digits without spaces (+123456789). ',
'introduction' => '0.15.0',
'default' => '',
'required' => false,
+4 -3
View File
@@ -87,8 +87,8 @@ $createSession = function (string $userId, string $secret, Request $request, Res
$factor = (match ($verifiedToken->getAttribute('type')) {
Auth::TOKEN_TYPE_MAGIC_URL,
Auth::TOKEN_TYPE_OAUTH2,
Auth::TOKEN_TYPE_EMAIL => 'email',
Auth::TOKEN_TYPE_PHONE => 'phone',
Auth::TOKEN_TYPE_EMAIL => Type::EMAIL,
Auth::TOKEN_TYPE_PHONE => Type::PHONE,
Auth::TOKEN_TYPE_GENERIC => 'token',
default => throw new Exception(Exception::USER_INVALID_TOKEN)
});
@@ -1520,7 +1520,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'factors' => ['email'],
'factors' => [TYPE::EMAIL, 'oauth2'], // include a special oauth2 factor to bypass MFA checks
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => DateTime::addSeconds(new \DateTime(), $duration)
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
@@ -2639,6 +2639,7 @@ Http::patch('/v1/account/password')
->label('sdk.response.model', Response::MODEL_USER)
->label('sdk.offline.model', '/account')
->label('sdk.offline.key', 'current')
->label('abuse-limit', 10)
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be at least 8 chars.', false, ['project', 'passwordsDictionary'])
->param('oldPassword', '', new Password(), 'Current user password. Must be at least 8 chars.', true)
->inject('requestTimestamp')
+1 -1
View File
@@ -884,7 +884,7 @@ Http::get('/v1/health/queue/failed/:name')
Event::FUNCTIONS_QUEUE_NAME,
Event::USAGE_QUEUE_NAME,
Event::USAGE_DUMP_QUEUE_NAME,
Event::WEBHOOK_CLASS_NAME,
Event::WEBHOOK_QUEUE_NAME,
Event::CERTIFICATES_QUEUE_NAME,
Event::BUILDS_QUEUE_NAME,
Event::MESSAGING_QUEUE_NAME,
+45 -51
View File
@@ -115,35 +115,8 @@ Http::post('/v1/projects')
$projectId = ($projectId == 'unique()') ? ID::unique() : $projectId;
$backups['database_db_fra1_v14x_02'] = ['from' => '03:00', 'to' => '05:00'];
$backups['database_db_fra1_v14x_03'] = ['from' => '00:00', 'to' => '02:00'];
$backups['database_db_fra1_v14x_04'] = ['from' => '00:00', 'to' => '02:00'];
$backups['database_db_fra1_v14x_05'] = ['from' => '00:00', 'to' => '02:00'];
$backups['database_db_fra1_v14x_06'] = ['from' => '00:00', 'to' => '02:00'];
$backups['database_db_fra1_v14x_07'] = ['from' => '00:00', 'to' => '02:00'];
$databases = Config::getParam('pools-database', []);
/**
* Remove databases from the list that are currently undergoing an backup
*/
if (count($databases) > 1) {
$now = new \DateTime();
foreach ($databases as $index => $database) {
if (empty($backups[$database])) {
continue;
}
$backup = $backups[$database];
$from = \DateTime::createFromFormat('H:i', $backup['from']);
$to = \DateTime::createFromFormat('H:i', $backup['to']);
if ($now >= $from && $now <= $to) {
unset($databases[$index]);
break;
}
}
}
$databaseOverride = System::getEnv('_APP_DATABASE_OVERRIDE');
$index = \array_search($databaseOverride, $databases);
if ($index !== false) {
@@ -156,39 +129,60 @@ Http::post('/v1/projects')
throw new Exception(Exception::PROJECT_RESERVED_PROJECT, "'console' is a reserved project.");
}
// TODO: 1 in 5 projects use shared tables. Temporary until all projects are using shared tables.
if (
(
!\mt_rand(0, 4)
&& System::getEnv('_APP_DATABASE_SHARED_TABLES', 'enabled') === 'enabled'
&& System::getEnv('_APP_EDITION', 'self-hosted') !== 'self-hosted'
) ||
($dsn === DATABASE_SHARED_TABLES)
) {
// TODO: Temporary until all projects are using shared tables.
if ($dsn === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$schema = 'appwrite';
$database = 'appwrite';
$namespace = System::getEnv('_APP_DATABASE_SHARED_NAMESPACE', '');
$dsn = $schema . '://' . DATABASE_SHARED_TABLES . '?database=' . $database;
$dsn = $schema . '://' . System::getEnv('_APP_DATABASE_SHARED_TABLES', '') . '?database=' . $database;
if (!empty($namespace)) {
$dsn .= '&namespace=' . $namespace;
}
}
// TODO: Allow overriding in development mode. Temporary until all projects are using shared tables.
if (
Http::isDevelopment()
&& System::getEnv('_APP_EDITION', 'self-hosted') !== 'self-hosted'
&& $request->getHeader('x-appwrited-share-tables', false)
) {
$schema = 'appwrite';
$database = 'appwrite';
$namespace = System::getEnv('_APP_DATABASE_SHARED_NAMESPACE', '');
$dsn = $schema . '://' . DATABASE_SHARED_TABLES . '?database=' . $database;
try {
$project = $dbForConsole->createDocument('projects', new Document([
'$id' => $projectId,
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'name' => $name,
'teamInternalId' => $team->getInternalId(),
'teamId' => $team->getId(),
'region' => $region,
'description' => $description,
'logo' => $logo,
'url' => $url,
'version' => APP_VERSION_STABLE,
'legalName' => $legalName,
'legalCountry' => $legalCountry,
'legalState' => $legalState,
'legalCity' => $legalCity,
'legalAddress' => $legalAddress,
'legalTaxId' => ID::custom($legalTaxId),
'services' => new stdClass(),
'platforms' => null,
'oAuthProviders' => [],
'webhooks' => null,
'keys' => null,
'auths' => $auths,
'search' => implode(' ', [$projectId, $name]),
'database' => $dsn,
]));
} catch (Duplicate) {
throw new Exception(Exception::PROJECT_ALREADY_EXISTS);
}
if (!empty($namespace)) {
$dsn .= '&namespace=' . $namespace;
}
try {
$dsn = new DSN($dsn);
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $dsn);
}
try {
@@ -251,7 +245,7 @@ Http::post('/v1/projects')
$dbForProject = new Database($adapter, $cache);
$dbForProject->setAuthorization($authorization);
if ($dsn->getHost() === DATABASE_SHARED_TABLES) {
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$dbForProject
->setSharedTables(true)
->setTenant($project->getInternalId())
+2 -2
View File
@@ -64,7 +64,7 @@ Http::post('/v1/storage/buckets')
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('fileSecurity', false, new Boolean(true), 'Enables configuring permissions for individual file. A user needs one of file or bucket level permissions to access a file. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true)
->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1024 * 1024, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1024 * 1024 * 1024), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan'])
->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1024 * 1024, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1024 * 1024), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan'])
->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true)
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
@@ -241,7 +241,7 @@ Http::put('/v1/storage/buckets/:bucketId')
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('fileSecurity', false, new Boolean(true), 'Enables configuring permissions for individual file. A user needs one of file or bucket level permissions to access a file. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true)
->param('maximumFileSize', null, new Range(1, (int) System::getEnv('_APP_STORAGE_LIMIT', 0)), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human((int)System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true)
->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1024 * 1024, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1024 * 1024), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan'])
->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true)
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
+2 -2
View File
@@ -1785,7 +1785,7 @@ Http::post('/v1/users/:userId/sessions')
throw new Exception(Exception::USER_NOT_FOUND);
}
$secret = Auth::codeGenerator();
$secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION);
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
@@ -1802,6 +1802,7 @@ Http::post('/v1/users/:userId/sessions')
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => $expire,
],
$detector->getOS(),
$detector->getClient(),
@@ -1813,7 +1814,6 @@ Http::post('/v1/users/:userId/sessions')
$session = $dbForProject->createDocument('sessions', $session);
$session
->setAttribute('secret', $secret)
->setAttribute('expire', $expire)
->setAttribute('countryName', $countryName);
$queueForEvents
+24 -3
View File
@@ -597,7 +597,7 @@ Http::init()
->addHeader('Server', 'Appwrite')
->addHeader('X-Content-Type-Options', 'nosniff')
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-Appwrite-Shared-Tables, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Forwarded-For, X-Forwarded-User-Agent')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Forwarded-For, X-Forwarded-User-Agent')
->addHeader('Access-Control-Expose-Headers', 'X-Appwrite-Session, X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Origin', $refDomain)
->addHeader('Access-Control-Allow-Credentials', 'true');
@@ -648,7 +648,7 @@ Http::options()
$response
->addHeader('Server', 'Appwrite')
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-Appwrite-Shared-Tables, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Appwrite-Session, X-Fallback-Cookies, X-Forwarded-For, X-Forwarded-User-Agent')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Appwrite-Session, X-Fallback-Cookies, X-Forwarded-For, X-Forwarded-User-Agent')
->addHeader('Access-Control-Expose-Headers', 'X-Appwrite-Session, X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Origin', $origin)
->addHeader('Access-Control-Allow-Credentials', 'true')
@@ -666,7 +666,8 @@ Http::error()
->inject('log')
->inject('authorization')
->inject('connections')
->action(function (Throwable $error, Document $user, ?Route $route, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Authorization $authorization, Connections $connections) {
->inject('queueForUsage')
->action(function (Throwable $error, Document $user, ?Route $route, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Authorization $authorization, Connections $connections, Usage $queueForUsage) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
if (is_null($route)) {
@@ -759,6 +760,26 @@ Http::error()
}
}
if ($publish && $project->getId() !== 'console') {
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$queueForUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize());
}
$queueForUsage
->setProject($project)
->trigger();
}
if ($logger && ($publish || $error->getCode() === 0)) {
if (isset($user) && !$user->isEmpty()) {
$log->setUser(new User($user->getId()));
+1698 -1708
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -60,7 +60,7 @@
"utopia-php/image": "0.6.*",
"utopia-php/locale": "0.4.*",
"utopia-php/logger": "0.5.*",
"utopia-php/messaging": "0.11.*",
"utopia-php/messaging": "0.12.*",
"utopia-php/migration": "0.4.*",
"utopia-php/orchestration": "dev-feat-framework-v2 as 0.9.99",
"utopia-php/platform": "dev-feat-framework-v2 as 0.5.99",
@@ -70,6 +70,7 @@
"utopia-php/queue": "dev-feat-coroutine-and-di as 0.7.99",
"utopia-php/registry": "0.5.*",
"utopia-php/storage": "dev-feat-framework-v2-v2 as 0.18.99",
"utopia-php/swoole": "0.8.*",
"utopia-php/system": "0.8.*",
"utopia-php/vcs": "0.6.*",
"utopia-php/websocket": "0.1.*",
Generated
+262 -537
View File
File diff suppressed because it is too large Load Diff
+18 -22
View File
@@ -189,6 +189,7 @@ services:
- _APP_CONSOLE_COUNTRIES_DENYLIST
- _APP_EXPERIMENT_LOGGING_PROVIDER
- _APP_EXPERIMENT_LOGGING_CONFIG
- _APP_DATABASE_SHARED_TABLES
appwrite-realtime:
entrypoint: realtime
@@ -238,6 +239,7 @@ services:
- _APP_USAGE_STATS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-audits:
entrypoint: worker-audits
@@ -267,6 +269,7 @@ services:
- _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-webhooks:
entrypoint: worker-webhooks
@@ -299,6 +302,7 @@ services:
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_WEBHOOK_MAX_FAILED_ATTEMPTS
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-deletes:
entrypoint: worker-deletes
@@ -356,6 +360,7 @@ services:
- _APP_LOGGING_CONFIG
- _APP_EXECUTOR_SECRET
- _APP_EXECUTOR_HOST
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-databases:
entrypoint: worker-databases
@@ -387,6 +392,7 @@ services:
- _APP_LOGGING_CONFIG
- _APP_WORKERS_NUM
- _APP_QUEUE_NAME
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-builds:
entrypoint: worker-builds
@@ -452,6 +458,7 @@ services:
- _APP_STORAGE_WASABI_SECRET
- _APP_STORAGE_WASABI_REGION
- _APP_STORAGE_WASABI_BUCKET
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-certificates:
entrypoint: worker-certificates
@@ -487,6 +494,7 @@ services:
- _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-functions:
entrypoint: worker-functions
@@ -526,6 +534,7 @@ services:
- _APP_DOCKER_HUB_PASSWORD
- _APP_LOGGING_CONFIG
- _APP_LOGGING_PROVIDER
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-mails:
entrypoint: worker-mails
@@ -560,6 +569,7 @@ services:
- _APP_LOGGING_CONFIG
- _APP_DOMAIN
- _APP_OPTIONS_FORCE_HTTPS
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-messaging:
entrypoint: worker-messaging
@@ -614,6 +624,7 @@ services:
- _APP_STORAGE_WASABI_SECRET
- _APP_STORAGE_WASABI_REGION
- _APP_STORAGE_WASABI_BUCKET
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-migrations:
entrypoint: worker-migrations
@@ -649,6 +660,7 @@ services:
- _APP_LOGGING_CONFIG
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
- _APP_DATABASE_SHARED_TABLES
appwrite-task-maintenance:
entrypoint: maintenance
@@ -687,6 +699,7 @@ services:
- _APP_MAINTENANCE_RETENTION_USAGE_HOURLY
- _APP_MAINTENANCE_RETENTION_SCHEDULES
- _APP_MAINTENANCE_DELAY
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-usage:
entrypoint: worker-usage
@@ -718,6 +731,7 @@ services:
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_USAGE_AGGREGATION_INTERVAL
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-usage-dump:
entrypoint: worker-usage-dump
@@ -749,6 +763,7 @@ services:
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_USAGE_AGGREGATION_INTERVAL
- _APP_DATABASE_SHARED_TABLES
appwrite-task-scheduler-functions:
entrypoint: schedule-functions
@@ -777,6 +792,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DATABASE_SHARED_TABLES
appwrite-task-scheduler-messages:
entrypoint: schedule-messages
@@ -805,6 +821,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DATABASE_SHARED_TABLES
appwrite-assistant:
container_name: appwrite-assistant
@@ -903,20 +920,7 @@ services:
- MYSQL_USER=${_APP_DB_USER}
- MYSQL_PASSWORD=${_APP_DB_PASS}
- MARIADB_AUTO_UPGRADE=1
command: "mysqld --innodb-flush-method=fsync" # add ' --query_cache_size=0' for DB tests
# command: mv /var/lib/mysql/ib_logfile0 /var/lib/mysql/ib_logfile0.bu && mv /var/lib/mysql/ib_logfile1 /var/lib/mysql/ib_logfile1.bu
# smtp:
# image: appwrite/smtp:1.2.0
# container_name: appwrite-smtp
# restart: unless-stopped
# networks:
# - appwrite
# environment:
# - LOCAL_DOMAINS=@
# - RELAY_FROM_HOSTS=192.168.0.0/16 ; *.yourdomain.com
# - SMARTHOST_HOST=smtp
# - SMARTHOST_PORT=587
command: "mysqld --innodb-flush-method=fsync"
redis:
image: redis:7.2.4-alpine
@@ -934,14 +938,6 @@ services:
volumes:
- appwrite-redis:/data:rw
# clamav:
# image: appwrite/clamav:1.2.0
# container_name: appwrite-clamav
# networks:
# - appwrite
# volumes:
# - appwrite-uploads:/storage/uploads
# Dev Tools Start ------------------------------------------------------------------------------------------
#
# The Appwrite Team uses the following tools to help debug, monitor and diagnose the Appwrite stack
@@ -1 +1 @@
Verify an authenticator app after adding it using the [add authenticator](/docs/references/cloud/client-web/account#createMfaAuthenticator) method. add
Verify an authenticator app after adding it using the [add authenticator](/docs/references/cloud/client-web/account#createMfaAuthenticator) method.
+2 -1
View File
@@ -235,7 +235,7 @@ abstract class Migration
}
/**
* Creates colletion from the config collection.
* Creates collection from the config collection.
*
* @param string $id
* @param string|null $name
@@ -262,6 +262,7 @@ abstract class Migration
'type' => $attribute['type'],
'size' => $attribute['size'],
'required' => $attribute['required'],
'default' => $attribute['default'] ?? null,
'signed' => $attribute['signed'],
'array' => $attribute['array'],
'filters' => $attribute['filters'],
+26
View File
@@ -336,6 +336,32 @@ class V20 extends Migration
Console::warning("Purge cache from {$id}: {$th->getMessage()}");
}
break;
case 'topics':
try {
$this->projectDB->updateAttributeDefault($id, 'emailTotal', 0);
} catch (Throwable $th) {
Console::warning("'topics' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->updateAttributeDefault($id, 'pushTotal', 0);
} catch (Throwable $th) {
Console::warning("'topics' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->updateAttributeDefault($id, 'smsTotal', 0);
} catch (Throwable $th) {
Console::warning("'topics' from {$id}: {$th->getMessage()}");
}
try {
$this->projectDB->purgeCachedCollection($id);
} catch (Throwable $th) {
Console::warning("Purge cache from {$id}: {$th->getMessage()}");
}
break;
}
+1
View File
@@ -91,6 +91,7 @@ class Migrate extends Action
// TODO: Iterate through all project DBs
/** @var Database $projectDB */
$projectDB = $getProjectDB($project);
$projectDB->disableValidation();
$migration
->setProject($project, $projectDB, $dbForConsole)
->setPDO($register->get('db', true))
+3 -3
View File
@@ -500,14 +500,14 @@ class Deletes extends Action
$collections = $dbForProject->listCollections($limit);
foreach ($collections as $collection) {
if ($dsn->getHost() !== DATABASE_SHARED_TABLES || !\in_array($collection->getId(), $projectCollectionIds)) {
if ($dsn->getHost() !== System::getEnv('_APP_DATABASE_SHARED_TABLES', '') || !\in_array($collection->getId(), $projectCollectionIds)) {
$dbForProject->deleteCollection($collection->getId());
} else {
$this->deleteByGroup($collection->getId(), [], database: $dbForProject);
}
}
if ($dsn->getHost() === DATABASE_SHARED_TABLES) {
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$collectionsIds = \array_map(fn ($collection) => $collection->getId(), $collections);
if (empty(\array_diff($collectionsIds, $projectCollectionIds))) {
@@ -556,7 +556,7 @@ class Deletes extends Action
], $dbForConsole);
// Delete metadata table
if ($dsn->getHost() !== DATABASE_SHARED_TABLES) {
if ($dsn->getHost() !== System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$dbForProject->deleteCollection('_metadata');
} else {
$this->deleteByGroup('_metadata', [], $dbForProject);
+13 -5
View File
@@ -399,7 +399,10 @@ class Messaging extends Action
'credentials' => match ($host) {
'twilio' => [
'accountSid' => $user,
'authToken' => $password
'authToken' => $password,
// Twilio Messaging Service SIDs always start with MG
// https://www.twilio.com/docs/messaging/services
'messagingServiceSid' => \str_starts_with($from, 'MG') ? $from : null
],
'textmagic' => [
'username' => $user,
@@ -420,9 +423,14 @@ class Messaging extends Action
],
default => null
},
'options' => [
'from' => $from
]
'options' => match ($host) {
'twilio' => [
'from' => \str_starts_with($from, 'MG') ? null : $from
],
default => [
'from' => $from
]
}
]);
$adapter = $this->getSmsAdapter($provider);
@@ -465,7 +473,7 @@ class Messaging extends Action
return match ($provider->getAttribute('provider')) {
'mock' => new Mock('username', 'password'),
'twilio' => new Twilio($credentials['accountSid'], $credentials['authToken']),
'twilio' => new Twilio($credentials['accountSid'], $credentials['authToken'], null, isset($credentials['messagingServiceSid']) ? $credentials['messagingServiceSid'] : null),
'textmagic' => new TextMagic($credentials['username'], $credentials['apiKey']),
'telesign' => new Telesign($credentials['customerId'], $credentials['apiKey']),
'msg91' => new Msg91($credentials['senderId'], $credentials['authKey'], $credentials['templateId']),
+1 -1
View File
@@ -31,7 +31,7 @@ class HTTPTest extends Scope
$this->assertEquals(204, $response['headers']['status-code']);
$this->assertEquals('Appwrite', $response['headers']['server']);
$this->assertEquals('GET, POST, PUT, PATCH, DELETE', $response['headers']['access-control-allow-methods']);
$this->assertEquals('Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-Appwrite-Shared-Tables, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Appwrite-Session, X-Fallback-Cookies, X-Forwarded-For, X-Forwarded-User-Agent', $response['headers']['access-control-allow-headers']);
$this->assertEquals('Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Appwrite-Session, X-Fallback-Cookies, X-Forwarded-For, X-Forwarded-User-Agent', $response['headers']['access-control-allow-headers']);
$this->assertEquals('X-Appwrite-Session, X-Fallback-Cookies', $response['headers']['access-control-expose-headers']);
$this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']);
$this->assertEquals('true', $response['headers']['access-control-allow-credentials']);
@@ -2,6 +2,7 @@
namespace Tests\E2E\Services\Functions;
use Appwrite\Tests\Retry;
use CURLFile;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
@@ -42,6 +43,7 @@ class FunctionsCustomClientTest extends Scope
return [];
}
#[Retry(count: 2)]
public function testCreateExecution(): array
{
/**
@@ -164,7 +166,7 @@ class FunctionsCustomClientTest extends Scope
$this->assertEquals(202, $execution['headers']['status-code']);
// Wait for the first scheduled execution to be created
sleep(65);
sleep(90);
$executions = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/executions', [
'content-type' => 'application/json',
@@ -988,7 +988,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals($executions['body']['executions'][0]['logs'], '');
$this->assertStringContainsString('timed out', $executions['body']['executions'][0]['errors']);
sleep(70); //wait for scheduled execution to be created and time out
sleep(75); // Wait for scheduled execution to be created and time out
$executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', array_merge([
'content-type' => 'application/json',
@@ -999,12 +999,6 @@ class FunctionsCustomServerTest extends Scope
$this->assertCount(2, $executions['body']['executions']);
$this->assertIsArray($executions['body']['executions']);
$this->assertEquals($executions['body']['executions'][1]['trigger'], 'schedule');
$this->assertEquals($executions['body']['executions'][1]['status'], 'failed');
$this->assertEquals($executions['body']['executions'][1]['responseStatusCode'], 500);
$this->assertLessThan(20, $executions['body']['executions'][1]['duration']);
$this->assertEquals($executions['body']['executions'][1]['responseBody'], '');
$this->assertEquals($executions['body']['executions'][1]['logs'], '');
$this->assertStringContainsString('timed out', $executions['body']['executions'][1]['errors']);
// Cleanup : Delete function
$response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [
@@ -455,7 +455,7 @@ class HealthCustomServerTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('/CN=www.google.com', $response['body']['name']);
$this->assertEquals('www.google.com', $response['body']['subjectSN']);
$this->assertEquals('Google Trust Services LLC', $response['body']['issuerOrganisation']);
$this->assertStringContainsString('Google Trust Services', $response['body']['issuerOrganisation']);
$this->assertIsInt($response['body']['validFrom']);
$this->assertIsInt($response['body']['validTo']);
@@ -511,6 +511,55 @@ trait MessagingBase
];
}
public function testSubscriberTargetSubQuery()
{
$response = $this->client->call(Client::METHOD_POST, '/messaging/topics', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'topicId' => 'sub-query-test',
'name' => 'sub-query-test',
]);
$this->assertEquals(201, $response['headers']['status-code']);
$topic = $response['body'];
$prefix = uniqid();
for ($i = 1; $i <= 101; $i++) {
$response = $this->client->call(Client::METHOD_POST, '/users', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'userId' => "$prefix-$i",
'email' => "$prefix-$i@example.com",
'password' => 'password',
'name' => "User $prefix $i",
]);
$this->assertEquals(201, $response['headers']['status-code']);
$user = $response['body'];
$targets = $user['targets'] ?? [];
$this->assertGreaterThan(0, count($targets));
$target = $targets[0];
$response = $this->client->call(Client::METHOD_POST, '/messaging/topics/' . $topic['$id'] . '/subscribers', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'subscriberId' => $user['$id'],
'targetId' => $target['$id'],
]);
$this->assertEquals(201, $response['headers']['status-code']);
}
}
/**
* @depends testCreateSubscriber
*/
@@ -9,7 +9,6 @@ use Tests\E2E\General\UsageTest;
use Tests\E2E\Scopes\ProjectConsole;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
@@ -3494,504 +3493,4 @@ class ProjectsConsoleClientTest extends Scope
return $data;
}
public function testTenantIsolation(): void
{
// Create a team and a project
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'teamId' => ID::unique(),
'name' => 'Amazing Team',
]);
$teamId = $team['body']['$id'];
// Project-level isolation
$project1 = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-shared-tables' => false
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Amazing Project',
'teamId' => $teamId,
'region' => 'default'
]);
// Application level isolation (shared tables)
$project2 = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-shared-tables' => true
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Amazing Project',
'teamId' => $teamId,
'region' => 'default'
]);
// Project-level isolation
$project3 = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-shared-tables' => false
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Amazing Project',
'teamId' => $teamId,
'region' => 'default'
]);
// Application level isolation (shared tables)
$project4 = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-shared-tables' => true
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Amazing Project',
'teamId' => $teamId,
'region' => 'default'
]);
// Create and API key in each project
$key1 = $this->client->call(Client::METHOD_POST, '/projects/' . $project1['body']['$id'] . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Key Test',
'scopes' => ['databases.read', 'databases.write', 'collections.read', 'collections.write', 'attributes.read', 'attributes.write', 'indexes.read', 'indexes.write', 'documents.read', 'documents.write', 'users.read', 'users.write'],
]);
$key2 = $this->client->call(Client::METHOD_POST, '/projects/' . $project2['body']['$id'] . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Key Test',
'scopes' => ['databases.read', 'databases.write', 'collections.read', 'collections.write', 'attributes.read', 'attributes.write', 'indexes.read', 'indexes.write', 'documents.read', 'documents.write', 'users.read', 'users.write'],
]);
$key3 = $this->client->call(Client::METHOD_POST, '/projects/' . $project3['body']['$id'] . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Key Test',
'scopes' => ['databases.read', 'databases.write', 'collections.read', 'collections.write', 'attributes.read', 'attributes.write', 'indexes.read', 'indexes.write', 'documents.read', 'documents.write', 'users.read', 'users.write'],
]);
$key4 = $this->client->call(Client::METHOD_POST, '/projects/' . $project4['body']['$id'] . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Key Test',
'scopes' => ['databases.read', 'databases.write', 'collections.read', 'collections.write', 'attributes.read', 'attributes.write', 'indexes.read', 'indexes.write', 'documents.read', 'documents.write', 'users.read', 'users.write'],
]);
// Create a database in each project
$database1 = $this->client->call(Client::METHOD_POST, '/databases', [
'content-type' => 'application/json',
'x-appwrite-project' => $project1['body']['$id'],
'x-appwrite-key' => $key1['body']['secret']
], [
'databaseId' => ID::unique(),
'name' => 'Amazing Database',
]);
$database2 = $this->client->call(Client::METHOD_POST, '/databases', [
'content-type' => 'application/json',
'x-appwrite-project' => $project2['body']['$id'],
'x-appwrite-key' => $key2['body']['secret']
], [
'databaseId' => ID::unique(),
'name' => 'Amazing Database',
]);
$database3 = $this->client->call(Client::METHOD_POST, '/databases', [
'content-type' => 'application/json',
'x-appwrite-project' => $project3['body']['$id'],
'x-appwrite-key' => $key3['body']['secret']
], [
'databaseId' => ID::unique(),
'name' => 'Amazing Database',
]);
$database4 = $this->client->call(Client::METHOD_POST, '/databases', [
'content-type' => 'application/json',
'x-appwrite-project' => $project4['body']['$id'],
'x-appwrite-key' => $key4['body']['secret']
], [
'databaseId' => ID::unique(),
'name' => 'Amazing Database',
]);
// Create a collection in each project
$collection1 = $this->client->call(Client::METHOD_POST, '/databases/' . $database1['body']['$id'] . '/collections', [
'content-type' => 'application/json',
'x-appwrite-project' => $project1['body']['$id'],
'x-appwrite-key' => $key1['body']['secret']
], [
'databaseId' => $database1['body']['$id'],
'collectionId' => ID::unique(),
'name' => 'Amazing Collection',
]);
$collection2 = $this->client->call(Client::METHOD_POST, '/databases/' . $database2['body']['$id'] . '/collections', [
'content-type' => 'application/json',
'x-appwrite-project' => $project2['body']['$id'],
'x-appwrite-key' => $key2['body']['secret']
], [
'databaseId' => $database2['body']['$id'],
'collectionId' => ID::unique(),
'name' => 'Amazing Collection',
]);
$collection3 = $this->client->call(Client::METHOD_POST, '/databases/' . $database3['body']['$id'] . '/collections', [
'content-type' => 'application/json',
'x-appwrite-project' => $project3['body']['$id'],
'x-appwrite-key' => $key3['body']['secret']
], [
'databaseId' => $database3['body']['$id'],
'collectionId' => ID::unique(),
'name' => 'Amazing Collection',
]);
$collection4 = $this->client->call(Client::METHOD_POST, '/databases/' . $database4['body']['$id'] . '/collections', [
'content-type' => 'application/json',
'x-appwrite-project' => $project4['body']['$id'],
'x-appwrite-key' => $key4['body']['secret']
], [
'databaseId' => $database4['body']['$id'],
'collectionId' => ID::unique(),
'name' => 'Amazing Collection',
]);
// Create an attribute in each project
$attribute1 = $this->client->call(Client::METHOD_POST, '/databases/' . $database1['body']['$id'] . '/collections/' . $collection1['body']['$id'] . '/attributes/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $project1['body']['$id'],
'x-appwrite-key' => $key1['body']['secret']
], [
'databaseId' => $database1['body']['$id'],
'collectionId' => $collection1['body']['$id'],
'key' => ID::unique(),
'size' => 255,
'required' => true
]);
$attribute2 = $this->client->call(Client::METHOD_POST, '/databases/' . $database2['body']['$id'] . '/collections/' . $collection2['body']['$id'] . '/attributes/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $project2['body']['$id'],
'x-appwrite-key' => $key2['body']['secret']
], [
'databaseId' => $database2['body']['$id'],
'collectionId' => $collection2['body']['$id'],
'key' => ID::unique(),
'size' => 255,
'required' => true
]);
$attribute3 = $this->client->call(Client::METHOD_POST, '/databases/' . $database3['body']['$id'] . '/collections/' . $collection3['body']['$id'] . '/attributes/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $project3['body']['$id'],
'x-appwrite-key' => $key3['body']['secret']
], [
'databaseId' => $database3['body']['$id'],
'collectionId' => $collection3['body']['$id'],
'key' => ID::unique(),
'size' => 255,
'required' => true
]);
$attribute4 = $this->client->call(Client::METHOD_POST, '/databases/' . $database4['body']['$id'] . '/collections/' . $collection4['body']['$id'] . '/attributes/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $project4['body']['$id'],
'x-appwrite-key' => $key4['body']['secret']
], [
'databaseId' => $database4['body']['$id'],
'collectionId' => $collection4['body']['$id'],
'key' => ID::unique(),
'size' => 255,
'required' => true
]);
// Wait for attributes
\sleep(2);
// Create an index in each project
$index1 = $this->client->call(Client::METHOD_POST, '/databases/' . $database1['body']['$id'] . '/collections/' . $collection1['body']['$id'] . '/indexes', [
'content-type' => 'application/json',
'x-appwrite-project' => $project1['body']['$id'],
'x-appwrite-key' => $key1['body']['secret']
], [
'databaseId' => $database1['body']['$id'],
'collectionId' => $collection1['body']['$id'],
'key' => ID::unique(),
'type' => Database::INDEX_KEY,
'attributes' => [$attribute1['body']['key']],
]);
$index2 = $this->client->call(Client::METHOD_POST, '/databases/' . $database2['body']['$id'] . '/collections/' . $collection2['body']['$id'] . '/indexes', [
'content-type' => 'application/json',
'x-appwrite-project' => $project2['body']['$id'],
'x-appwrite-key' => $key2['body']['secret']
], [
'databaseId' => $database2['body']['$id'],
'collectionId' => $collection2['body']['$id'],
'key' => ID::unique(),
'type' => Database::INDEX_KEY,
'attributes' => [$attribute2['body']['key']],
]);
$index3 = $this->client->call(Client::METHOD_POST, '/databases/' . $database3['body']['$id'] . '/collections/' . $collection3['body']['$id'] . '/indexes', [
'content-type' => 'application/json',
'x-appwrite-project' => $project3['body']['$id'],
'x-appwrite-key' => $key3['body']['secret']
], [
'databaseId' => $database3['body']['$id'],
'collectionId' => $collection3['body']['$id'],
'key' => ID::unique(),
'type' => Database::INDEX_KEY,
'attributes' => [$attribute3['body']['key']],
]);
$index4 = $this->client->call(Client::METHOD_POST, '/databases/' . $database4['body']['$id'] . '/collections/' . $collection4['body']['$id'] . '/indexes', [
'content-type' => 'application/json',
'x-appwrite-project' => $project4['body']['$id'],
'x-appwrite-key' => $key4['body']['secret']
], [
'databaseId' => $database4['body']['$id'],
'collectionId' => $collection4['body']['$id'],
'key' => ID::unique(),
'type' => Database::INDEX_KEY,
'attributes' => [$attribute4['body']['key']],
]);
// Wait for indexes
\sleep(2);
// Assert that each project has only 1 database, 1 collection, 1 attribute and 1 index
$databasesProject1 = $this->client->call(Client::METHOD_GET, '/databases', [
'content-type' => 'application/json',
'x-appwrite-project' => $project1['body']['$id'],
'x-appwrite-key' => $key1['body']['secret']
]);
$this->assertEquals(1, $databasesProject1['body']['total']);
$this->assertEquals(1, \count($databasesProject1['body']['databases']));
$databasesProject2 = $this->client->call(Client::METHOD_GET, '/databases', [
'content-type' => 'application/json',
'x-appwrite-project' => $project2['body']['$id'],
'x-appwrite-key' => $key2['body']['secret']
]);
$this->assertEquals(1, $databasesProject2['body']['total']);
$this->assertEquals(1, \count($databasesProject2['body']['databases']));
$databasesProject3 = $this->client->call(Client::METHOD_GET, '/databases', [
'content-type' => 'application/json',
'x-appwrite-project' => $project3['body']['$id'],
'x-appwrite-key' => $key3['body']['secret']
]);
$this->assertEquals(1, $databasesProject3['body']['total']);
$this->assertEquals(1, \count($databasesProject3['body']['databases']));
$databasesProject4 = $this->client->call(Client::METHOD_GET, '/databases', [
'content-type' => 'application/json',
'x-appwrite-project' => $project4['body']['$id'],
'x-appwrite-key' => $key4['body']['secret']
]);
$this->assertEquals(1, $databasesProject4['body']['total']);
$this->assertEquals(1, \count($databasesProject4['body']['databases']));
$collectionsProject1 = $this->client->call(Client::METHOD_GET, '/databases/' . $database1['body']['$id'] . '/collections', [
'content-type' => 'application/json',
'x-appwrite-project' => $project1['body']['$id'],
'x-appwrite-key' => $key1['body']['secret']
]);
$this->assertEquals(1, $collectionsProject1['body']['total']);
$this->assertEquals(1, \count($collectionsProject1['body']['collections']));
$collectionsProject2 = $this->client->call(Client::METHOD_GET, '/databases/' . $database2['body']['$id'] . '/collections', [
'content-type' => 'application/json',
'x-appwrite-project' => $project2['body']['$id'],
'x-appwrite-key' => $key2['body']['secret']
]);
$this->assertEquals(1, $collectionsProject2['body']['total']);
$this->assertEquals(1, \count($collectionsProject2['body']['collections']));
$collectionsProject3 = $this->client->call(Client::METHOD_GET, '/databases/' . $database3['body']['$id'] . '/collections', [
'content-type' => 'application/json',
'x-appwrite-project' => $project3['body']['$id'],
'x-appwrite-key' => $key3['body']['secret']
]);
$this->assertEquals(1, $collectionsProject3['body']['total']);
$this->assertEquals(1, \count($collectionsProject3['body']['collections']));
$collectionsProject4 = $this->client->call(Client::METHOD_GET, '/databases/' . $database4['body']['$id'] . '/collections', [
'content-type' => 'application/json',
'x-appwrite-project' => $project4['body']['$id'],
'x-appwrite-key' => $key4['body']['secret']
]);
$this->assertEquals(1, $collectionsProject4['body']['total']);
$this->assertEquals(1, \count($collectionsProject4['body']['collections']));
$attributesProject1 = $this->client->call(Client::METHOD_GET, '/databases/' . $database1['body']['$id'] . '/collections/' . $collection1['body']['$id'] . '/attributes', [
'content-type' => 'application/json',
'x-appwrite-project' => $project1['body']['$id'],
'x-appwrite-key' => $key1['body']['secret']
]);
$this->assertEquals(1, $attributesProject1['body']['total']);
$this->assertEquals(1, \count($attributesProject1['body']['attributes']));
$this->assertEquals('available', $attributesProject1['body']['attributes'][0]['status']);
$attributesProject2 = $this->client->call(Client::METHOD_GET, '/databases/' . $database2['body']['$id'] . '/collections/' . $collection2['body']['$id'] . '/attributes', [
'content-type' => 'application/json',
'x-appwrite-project' => $project2['body']['$id'],
'x-appwrite-key' => $key2['body']['secret']
]);
$this->assertEquals(1, $attributesProject2['body']['total']);
$this->assertEquals(1, \count($attributesProject2['body']['attributes']));
$this->assertEquals('available', $attributesProject2['body']['attributes'][0]['status']);
$attributesProject3 = $this->client->call(Client::METHOD_GET, '/databases/' . $database3['body']['$id'] . '/collections/' . $collection3['body']['$id'] . '/attributes', [
'content-type' => 'application/json',
'x-appwrite-project' => $project3['body']['$id'],
'x-appwrite-key' => $key3['body']['secret']
]);
$this->assertEquals(1, $attributesProject3['body']['total']);
$this->assertEquals(1, \count($attributesProject3['body']['attributes']));
$this->assertEquals('available', $attributesProject3['body']['attributes'][0]['status']);
$attributesProject4 = $this->client->call(Client::METHOD_GET, '/databases/' . $database4['body']['$id'] . '/collections/' . $collection4['body']['$id'] . '/attributes', [
'content-type' => 'application/json',
'x-appwrite-project' => $project4['body']['$id'],
'x-appwrite-key' => $key4['body']['secret']
]);
$this->assertEquals(1, $attributesProject4['body']['total']);
$this->assertEquals(1, \count($attributesProject4['body']['attributes']));
$this->assertEquals('available', $attributesProject4['body']['attributes'][0]['status']);
$indexesProject1 = $this->client->call(Client::METHOD_GET, '/databases/' . $database1['body']['$id'] . '/collections/' . $collection1['body']['$id'] . '/indexes', [
'content-type' => 'application/json',
'x-appwrite-project' => $project1['body']['$id'],
'x-appwrite-key' => $key1['body']['secret']
]);
$this->assertEquals(1, $indexesProject1['body']['total']);
$this->assertEquals(1, \count($indexesProject1['body']['indexes']));
$indexesProject2 = $this->client->call(Client::METHOD_GET, '/databases/' . $database2['body']['$id'] . '/collections/' . $collection2['body']['$id'] . '/indexes', [
'content-type' => 'application/json',
'x-appwrite-project' => $project2['body']['$id'],
'x-appwrite-key' => $key2['body']['secret']
]);
$this->assertEquals(1, $indexesProject2['body']['total']);
$this->assertEquals(1, \count($indexesProject2['body']['indexes']));
$indexesProject3 = $this->client->call(Client::METHOD_GET, '/databases/' . $database3['body']['$id'] . '/collections/' . $collection3['body']['$id'] . '/indexes', [
'content-type' => 'application/json',
'x-appwrite-project' => $project3['body']['$id'],
'x-appwrite-key' => $key3['body']['secret']
]);
$this->assertEquals(1, $indexesProject3['body']['total']);
$this->assertEquals(1, \count($indexesProject3['body']['indexes']));
$indexesProject4 = $this->client->call(Client::METHOD_GET, '/databases/' . $database4['body']['$id'] . '/collections/' . $collection4['body']['$id'] . '/indexes', [
'content-type' => 'application/json',
'x-appwrite-project' => $project4['body']['$id'],
'x-appwrite-key' => $key4['body']['secret']
]);
$this->assertEquals(1, $indexesProject4['body']['total']);
$this->assertEquals(1, \count($indexesProject4['body']['indexes']));
// Attempt to read cross-type resources
$collectionProject2WithProject1Key = $this->client->call(Client::METHOD_GET, '/databases/' . $database2['body']['$id'] . '/collections/' . $collection2['body']['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $project1['body']['$id'],
'x-appwrite-key' => $key1['body']['secret']
]);
$this->assertEquals(404, $collectionProject2WithProject1Key['headers']['status-code']);
$collectionProject1WithProject2Key = $this->client->call(Client::METHOD_GET, '/databases/' . $database1['body']['$id'] . '/collections/' . $collection1['body']['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $project2['body']['$id'],
'x-appwrite-key' => $key2['body']['secret']
]);
$this->assertEquals(404, $collectionProject1WithProject2Key['headers']['status-code']);
// Attempt to read cross-tenant resources
$collectionProject3WithProject1Key = $this->client->call(Client::METHOD_GET, '/databases/' . $database3['body']['$id'] . '/collections/' . $collection3['body']['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $project1['body']['$id'],
'x-appwrite-key' => $key1['body']['secret']
]);
$this->assertEquals(404, $collectionProject3WithProject1Key['headers']['status-code']);
$collectionProject1WithProject3Key = $this->client->call(Client::METHOD_GET, '/databases/' . $database1['body']['$id'] . '/collections/' . $collection1['body']['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $project3['body']['$id'],
'x-appwrite-key' => $key3['body']['secret']
]);
$this->assertEquals(404, $collectionProject1WithProject3Key['headers']['status-code']);
// Assert that shared project resources can have the same ID as they're unique on tenant + ID not just ID
$collection5 = $this->client->call(Client::METHOD_POST, '/databases/' . $database2['body']['$id'] . '/collections', [
'content-type' => 'application/json',
'x-appwrite-project' => $project2['body']['$id'],
'x-appwrite-key' => $key2['body']['secret']
], [
'databaseId' => $database2['body']['$id'],
'collectionId' => $collection4['body']['$id'],
'name' => 'Amazing Collection',
]);
$this->assertEquals(201, $collection5['headers']['status-code']);
// Assert that users across projects on shared tables can have the same email as they're unique on tenant + email not just email
$user1 = $this->client->call(Client::METHOD_POST, '/users', [
'content-type' => 'application/json',
'x-appwrite-project' => $project2['body']['$id'],
'x-appwrite-key' => $key2['body']['secret']
], [
'userId' => 'user',
'email' => 'test@appwrite.io',
'password' => 'password',
'name' => 'Test User',
]);
$this->assertEquals(201, $user1['headers']['status-code']);
$user2 = $this->client->call(Client::METHOD_POST, '/users', [
'content-type' => 'application/json',
'x-appwrite-project' => $project4['body']['$id'],
'x-appwrite-key' => $key4['body']['secret']
], [
'userId' => 'user',
'email' => 'test@appwrite.io',
'password' => 'password',
'name' => 'Test User',
]);
$this->assertEquals(201, $user2['headers']['status-code']);
}
}
+1 -1
View File
@@ -225,7 +225,7 @@ trait StorageBase
'bucketId' => ID::unique(),
'name' => 'Test Bucket 2',
'fileSecurity' => true,
'maximumFileSize' => 200000000, //200MB
'maximumFileSize' => 6000000000, //6GB
'allowedFileExtensions' => ["jpg", "png"],
'permissions' => [
Permission::read(Role::any()),
+23 -1
View File
@@ -290,6 +290,28 @@ trait UsersBase
$this->assertArrayNotHasKey('secret', $token['body']);
}
/**
* @depends testCreateUser
*/
public function testCreateSession(array $data): void
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_POST, '/users/' . $data['userId'] . '/sessions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(201, $response['headers']['status-code']);
$session = $response['body'];
$this->assertEquals($data['userId'], $session['userId']);
$this->assertNotEmpty($session['secret']);
$this->assertNotEmpty($session['expire']);
$this->assertEquals('server', $session['provider']);
}
/**
* Tests all optional parameters of createUser (email, phone, anonymous..)
@@ -986,7 +1008,7 @@ trait UsersBase
'password' => 'password'
]);
$this->assertEquals($session['headers']['status-code'], 401);
$this->assertEquals(401, $session['headers']['status-code']);
$user = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/password', array_merge([
'content-type' => 'application/json',